blob: 806caa40a39d63e7f88ad20b7161a9fa23b5a630 [file] [log] [blame]
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");
}
}
}
}
}