| package fuchsia.developer.plugin.fidl; |
| |
| import com.google.common.collect.ImmutableSet; |
| 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.psi.PsiElement; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.tree.TokenSet; |
| import fuchsia.developer.plugin.fidl.psi.Types; |
| import java.util.Arrays; |
| import java.util.Set; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| public class ContextAwareHighlighter implements Annotator { |
| |
| private static final TextAttributesKey KEYWORD_ATTRIBUTE; |
| private static final Set<String> TYPE_KEYWORDS; |
| 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]; |
| |
| ImmutableSet.Builder<String> builder = ImmutableSet.builder(); |
| builder.addAll( |
| Arrays.asList( |
| "array", |
| "vector", |
| "string", |
| "handle", |
| "process", |
| "thread", |
| "vmo", |
| "channel", |
| "event", |
| "port", |
| "interrupt", |
| "log", |
| "socket", |
| "resource", |
| "eventpair", |
| "exception", |
| "job", |
| "vmar", |
| "fifo", |
| "guest", |
| "timer", |
| "request", |
| "strict", |
| "bool", |
| "float32", |
| "float64", |
| "int8", |
| "int16", |
| "int32", |
| "int64", |
| "uint8", |
| "uint16", |
| "uint32", |
| "uint64")); |
| TYPE_KEYWORDS = builder.build(); |
| |
| 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 || lval < 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; |
| } |
| |
| @Override |
| public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { |
| PsiElement parent = element.getParent(); |
| if (parent == null || parent.getNode() == null) { |
| return; |
| } |
| IElementType parentType = parent.getNode().getElementType(); |
| IElementType thisType = element.getNode().getElementType(); |
| |
| // 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) { |
| IElementType grandParentType = parent.getParent().getNode().getElementType(); |
| if (grandParentType == Types.TYPE_CONSTRUCTOR) { |
| if (TYPE_KEYWORDS.contains(element.getText())) { |
| Annotation annotation = holder.createInfoAnnotation(element, null); |
| annotation.setTextAttributes(KEYWORD_ATTRIBUTE); |
| } |
| } |
| } |
| |
| // The enum-declaration allows the more liberal type-constructor in the grammar, but the |
| // compiler limits this to signed or unsigned integer types |
| if (parentType == Types.ENUM_DECLARATION && thisType == Types.TYPE_CONSTRUCTOR) { |
| // compound-identifier -> identifier-token -> int |
| ASTNode compoundIdentifier = element.getNode().findChildByType(Types.COMPOUND_IDENTIFIER); |
| if (compoundIdentifier != null) { |
| ASTNode identifierToken = compoundIdentifier.findChildByType(Types.IDENTIFIER_TOKEN); |
| |
| if (identifierToken != null && identifierToken.findChildByType(INTEGRAL_TYPES) == null) { |
| holder.createErrorAnnotation( |
| element, "Expected integral type, found " + identifierToken.getText()); |
| } |
| } |
| } |
| |
| if (thisType == Types.XUNION) { |
| holder.createWarningAnnotation( |
| element, |
| "Xunions are transitional, and will be renamed to unions" |
| + " in a future language revision"); |
| } |
| |
| // The bits-declaration allows the more liberal type-constructor in the grammar, but the |
| // compiler limits this to unsigned integer types |
| if (parentType == Types.BITS_DECLARATION && thisType == Types.TYPE_CONSTRUCTOR) { |
| // type-constructor -> compound-identifier -> identifier-token -> int8-or-whatever |
| ASTNode compoundIdentifier = element.getNode().findChildByType(Types.COMPOUND_IDENTIFIER); |
| if (compoundIdentifier != null) { |
| ASTNode identifierToken = compoundIdentifier.findChildByType(Types.IDENTIFIER_TOKEN); |
| if (identifierToken != null |
| && identifierToken.findChildByType(UNSIGNED_INTEGRAL_TYPES) == null) { |
| holder.createErrorAnnotation( |
| element, "Expected unsigned integral type, found " + identifierToken.getText()); |
| } |
| } |
| } |
| |
| // The rule from the grammar: |
| // ----- |
| // The bits-or-enum-member-value 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.BITS_OR_ENUM_MEMBER_VALUE) { |
| ASTNode grandParent = element.getParent().getParent().getNode(); |
| IElementType grandParentType = grandParent.getElementType(); |
| if (grandParentType == Types.ENUM_DECLARATION || grandParentType == Types.BITS_DECLARATION) { |
| // 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 { |
| // bits/enum-declaration -> bits-or-enum-member -> bits-or-enum-member-value |
| String value = |
| correctTypeOrError( |
| grandParent.findChildByType(Types.TYPE_CONSTRUCTOR), integralLiteral); |
| if (value != null) { |
| holder.createErrorAnnotation(element, value); |
| } |
| if (grandParentType == Types.BITS_DECLARATION) { |
| value = unsignedLongPowerOfTwoOrError(integralLiteral); |
| if (value != null) { |
| holder.createErrorAnnotation(element, value); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Attributes cannot be placed on a reserved member. |
| if (thisType == Types.RESERVED) { |
| // Parent is TABLE_FIELD_DECL, Grandparent is TABLE_FIELD |
| ASTNode grandParent = element.getParent().getParent().getNode(); |
| if (grandParent.getElementType() == Types.TABLE_FIELD) { |
| ASTNode maybeAttributeList = grandParent.findChildByType(Types.ATTRIBUTE_LIST); |
| if (maybeAttributeList != null) { |
| holder.createErrorAnnotation( |
| maybeAttributeList, "Attributes are not allowed on reserved table members"); |
| } |
| } |
| } |
| } |
| } |