diff --git a/liquidjava-example/src/main/java/testSuite/CorrectEnumField.java b/liquidjava-example/src/main/java/testSuite/CorrectEnumField.java new file mode 100644 index 00000000..05c1d21d --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/CorrectEnumField.java @@ -0,0 +1,23 @@ +package testSuite; + +import liquidjava.specification.Refinement; + +@SuppressWarnings("unused") +class CorrectEnumField { + enum Color { + Red, Green, Blue + } + + @Refinement("color != Color.Blue") + Color color; + + void setColor(@Refinement("c != Color.Blue") Color c) { + color = c; + } + + public static void main(String[] args) { + CorrectEnumField cef = new CorrectEnumField(); + cef.setColor(Color.Red); // correct + cef.setColor(Color.Green); // correct + } +} diff --git a/liquidjava-example/src/main/java/testSuite/CorrectEnumParam.java b/liquidjava-example/src/main/java/testSuite/CorrectEnumParam.java new file mode 100644 index 00000000..7e942c20 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/CorrectEnumParam.java @@ -0,0 +1,19 @@ +package testSuite; + +import liquidjava.specification.Refinement; + +@SuppressWarnings("unused") +class CorrectEnumParam { + enum Status { + Active, Inactive, Pending + } + + Status process(@Refinement("status == Status.Inactive") Status status) { + return Status.Active; + } + + public static void main(String[] args) { + CorrectEnumParam cep = new CorrectEnumParam(); + cep.process(Status.Inactive); // correct + } +} \ No newline at end of file diff --git a/liquidjava-example/src/main/java/testSuite/CorrectEnumRefinement.java b/liquidjava-example/src/main/java/testSuite/CorrectEnumRefinement.java new file mode 100644 index 00000000..3b261d8e --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/CorrectEnumRefinement.java @@ -0,0 +1,15 @@ +package testSuite; + +import liquidjava.specification.Refinement; + +class CorrectEnumRefinement { + enum Lever { + Up, Down, Middle + } + + public static void main(String[] args) { + @Refinement("_==Lever.Up || _==Lever.Down") + Lever lever = Lever.Up; + System.out.println(lever); + } +} diff --git a/liquidjava-example/src/main/java/testSuite/CorrectEnumUsage.java b/liquidjava-example/src/main/java/testSuite/CorrectEnumUsage.java new file mode 100644 index 00000000..87c7cdce --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/CorrectEnumUsage.java @@ -0,0 +1,45 @@ +package testSuite; + +import liquidjava.specification.StateRefinement; +import liquidjava.specification.StateSet; + +@SuppressWarnings("unused") +@StateSet({"photoMode", "videoMode", "noMode"}) +class CorrectEnumUsage { + enum Mode { + Photo, Video, Unknown + } + + Mode mode; + @StateRefinement(to="noMode(this)") + public CorrectEnumUsage() {} + + @StateRefinement(from="noMode(this) && mode == Mode.Photo", to="photoMode(this)") + @StateRefinement(from="noMode(this) && mode == Mode.Video", to="videoMode(this)") + public void setMode(Mode mode) { + this.mode = mode; + } + + @StateRefinement(from="photoMode(this)", to="noMode(this)") + @StateRefinement(from="videoMode(this)", to="noMode(this)") + public void resetMode() { + this.mode = null; + } + + @StateRefinement(from="photoMode(this)") + public void takePhoto() {} + + @StateRefinement(from="videoMode(this)") + public void takeVideo() {} + + + public static void main(String[] args) { + // Correct + CorrectEnumUsage st = new CorrectEnumUsage(); + st.setMode(Mode.Photo); // noMode -> photoMode + st.takePhoto(); + st.resetMode(); // photoMode -> noMode + st.setMode(Mode.Video); // noMode -> videoMode + st.takeVideo(); + } +} \ No newline at end of file diff --git a/liquidjava-example/src/main/java/testSuite/ErrorEnumFunctionRefinement.java b/liquidjava-example/src/main/java/testSuite/ErrorEnumFunctionRefinement.java new file mode 100644 index 00000000..4698f33c --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/ErrorEnumFunctionRefinement.java @@ -0,0 +1,22 @@ +// Refinement Error +package testSuite; + +import liquidjava.specification.Refinement; + +@SuppressWarnings("unused") +class ErrorEnumFunctionRefinement { + enum Color { Red, Green, Blue } + + Color c; + + Color changeColor(@Refinement("newColor == Color.Red || newColor == Color.Green") Color newColor) { + c = newColor; // correct + return c; + } + + public static void main(String[] args) { + ErrorEnumFunctionRefinement e = new ErrorEnumFunctionRefinement(); + e.changeColor(Color.Red); // correct + e.changeColor(Color.Blue); // error + } +} \ No newline at end of file diff --git a/liquidjava-example/src/main/java/testSuite/ErrorEnumNegation.java b/liquidjava-example/src/main/java/testSuite/ErrorEnumNegation.java new file mode 100644 index 00000000..3534bf24 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/ErrorEnumNegation.java @@ -0,0 +1,19 @@ +// Refinement Error +package testSuite; + +import liquidjava.specification.Refinement; + +@SuppressWarnings("unused") +class ErrorEnumNegation { + enum Status { + Active, Inactive, Pending + } + + void process(@Refinement("status != Status.Inactive") Status status) {} + + public static void main(String[] args) { + ErrorEnumNegation e = new ErrorEnumNegation(); + e.process(Status.Active); // correct + e.process(Status.Inactive); // error + } +} diff --git a/liquidjava-example/src/main/java/testSuite/ErrorEnumNull.java b/liquidjava-example/src/main/java/testSuite/ErrorEnumNull.java new file mode 100644 index 00000000..25e938d9 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/ErrorEnumNull.java @@ -0,0 +1,16 @@ +// Refinement Error +package testSuite; + +import liquidjava.specification.Refinement; + +@SuppressWarnings("unused") +class ErrorEnumNull { + enum Color { + Red, Green, Blue + } + + public static void main(String[] args) { + @Refinement("c == Color.Red || c == Color.Green") + Color c = null; // error + } +} \ No newline at end of file diff --git a/liquidjava-example/src/main/java/testSuite/ErrorEnumUsage.java b/liquidjava-example/src/main/java/testSuite/ErrorEnumUsage.java new file mode 100644 index 00000000..57e6818e --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/ErrorEnumUsage.java @@ -0,0 +1,35 @@ +// State Refinement Error +package testSuite; + +import liquidjava.specification.StateRefinement; +import liquidjava.specification.StateSet; + + +@SuppressWarnings("unused") +@StateSet({"photoMode", "videoMode", "noMode"}) +class ErrorEnumUsage { + enum Mode { + Photo, Video, Unknown + } + + Mode mode; + @StateRefinement(to="noMode(this)") + public ErrorEnumUsage() {} + + @StateRefinement(from="noMode(this) && mode == Mode.Photo", to="photoMode(this)") + @StateRefinement(from="noMode(this) && mode == Mode.Video", to="videoMode(this)") + public void setMode(Mode mode) { + this.mode = mode; + } + + @StateRefinement(from="photoMode(this)") + public void takePhoto() {} + + + public static void main(String[] args) { + // Correct + ErrorEnumUsage st = new ErrorEnumUsage(); + st.setMode(Mode.Video); + st.takePhoto(); //error + } +} \ No newline at end of file diff --git a/liquidjava-verifier/src/main/antlr4/rj/grammar/RJ.g4 b/liquidjava-verifier/src/main/antlr4/rj/grammar/RJ.g4 index d6f7f45f..ebb4c73f 100644 --- a/liquidjava-verifier/src/main/antlr4/rj/grammar/RJ.g4 +++ b/liquidjava-verifier/src/main/antlr4/rj/grammar/RJ.g4 @@ -38,12 +38,14 @@ literalExpression: | literal #lit | ID #var | functionCall #invocation + | enumerate #enum ; - functionCall: +functionCall: ghostCall | aliasCall - | dotCall; + | dotCall + ; dotCall: OBJECT_TYPE '(' args? ')' @@ -55,6 +57,9 @@ ghostCall: aliasCall: ID_UPPER '(' args? ')'; +enumerate: + ENUM; + args: pred (',' pred)* ; @@ -94,6 +99,7 @@ ARITHOP : '+'|'*'|'/'|'%';//|'-'; BOOL : 'true' | 'false'; ID_UPPER: ([A-Z][a-zA-Z0-9]*); +ENUM: [A-Z][a-zA-Z0-9_]* '.' [A-Z][a-zA-Z0-9_]*; OBJECT_TYPE: (([a-zA-Z][a-zA-Z0-9]+) ('.' [a-zA-Z][a-zA-Z0-9]*)+); ID : '#'*[a-zA-Z_][a-zA-Z0-9_#]*; diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/MethodsFirstChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/MethodsFirstChecker.java index e764d453..581b4644 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/MethodsFirstChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/MethodsFirstChecker.java @@ -7,8 +7,13 @@ import liquidjava.diagnostics.errors.LJError; import liquidjava.processor.context.Context; import liquidjava.processor.refinement_checker.general_checkers.MethodsFunctionsChecker; +import liquidjava.rj_language.Predicate; +import liquidjava.utils.constants.Formats; +import liquidjava.utils.constants.Types; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtConstructor; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtEnumValue; import spoon.reflect.declaration.CtInterface; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; @@ -116,4 +121,15 @@ public void visitCtMethod(CtMethod method) { } context.exitContext(); } + + @Override + public > void visitCtEnum(CtEnum enumRead) { + String enumName = enumRead.getSimpleName(); + String qualifiedEnumName = enumRead.getQualifiedName(); + for (CtEnumValue ev : enumRead.getEnumValues()) { + String varName = String.format(Formats.ENUM, enumName, ev.getSimpleName()); + context.addGlobalVariableToContext(varName, qualifiedEnumName, enumRead.getReference(), null); + } + super.visitCtEnum(enumRead); + } } diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java index d3d95bbd..82988819 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java @@ -269,7 +269,11 @@ public void visitCtFieldRead(CtFieldRead fieldRead) { String targetName = fieldRead.getTarget().toString(); fieldRead.putMetadata(Keys.REFINEMENT, Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), BuiltinFunctionPredicate.length(targetName, fieldRead))); - + } else if (fieldRead.getVariable().getDeclaringType().isEnum()) { + String target = fieldRead.getVariable().getDeclaringType().getSimpleName(); + String enumLiteral = String.format(Formats.ENUM, target, fieldName); + fieldRead.putMetadata(Keys.REFINEMENT, + Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), Predicate.createVar(enumLiteral))); } else { fieldRead.putMetadata(Keys.REFINEMENT, new Predicate()); // TODO DO WE WANT THIS OR TO SHOW ERROR MESSAGE? diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/Enum.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/Enum.java new file mode 100644 index 00000000..17c8e17b --- /dev/null +++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/ast/Enum.java @@ -0,0 +1,74 @@ +package liquidjava.rj_language.ast; + +import java.util.List; + +import liquidjava.diagnostics.errors.LJError; +import liquidjava.rj_language.visitors.ExpressionVisitor; + +public class Enum extends Expression { + + private final String typeName; + private final String constName; + + public Enum(String typeName, String constName) { + this.typeName = typeName; + this.constName = constName; + } + + public String getTypeName() { + return typeName; + } + + public String getConstName() { + return constName; + } + + @Override + public T accept(ExpressionVisitor visitor) throws LJError { + return visitor.visitEnum(this); + } + + @Override + public void getVariableNames(List toAdd) { + // end leaf + } + + @Override + public void getStateInvocations(List toAdd, List all) { + // end leaf + } + + @Override + public boolean isBooleanTrue() { + return false; + } + + @Override + public String toString() { + return typeName + "." + constName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((typeName == null) ? 0 : typeName.hashCode()); + result = prime * result + ((constName == null) ? 0 : constName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + Enum other = (Enum) obj; + return typeName.equals(other.typeName) && constName.equals(other.constName); + } + + @Override + public Expression clone() { + return new Enum(typeName, constName); + } +} diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/CreateASTVisitor.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/CreateASTVisitor.java index d906772c..5efee64f 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/CreateASTVisitor.java +++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/CreateASTVisitor.java @@ -7,6 +7,7 @@ import liquidjava.diagnostics.errors.SyntaxError; import liquidjava.rj_language.ast.AliasInvocation; import liquidjava.rj_language.ast.BinaryExpression; +import liquidjava.rj_language.ast.Enum; import liquidjava.rj_language.ast.Expression; import liquidjava.rj_language.ast.FunctionInvocation; import liquidjava.rj_language.ast.GroupExpression; @@ -19,6 +20,7 @@ import liquidjava.rj_language.ast.UnaryExpression; import liquidjava.rj_language.ast.Var; import liquidjava.utils.Utils; +import liquidjava.utils.constants.Formats; import liquidjava.utils.constants.Keys; import org.antlr.v4.runtime.tree.ParseTree; @@ -26,6 +28,7 @@ import rj.grammar.RJParser.AliasCallContext; import rj.grammar.RJParser.ArgsContext; import rj.grammar.RJParser.DotCallContext; +import rj.grammar.RJParser.EnumContext; import rj.grammar.RJParser.ExpBoolContext; import rj.grammar.RJParser.ExpContext; import rj.grammar.RJParser.ExpGroupContext; @@ -156,9 +159,10 @@ private Expression literalExpressionCreate(ParseTree rc) throws LJError { return new GroupExpression(create(((LitGroupContext) rc).literalExpression())); else if (rc instanceof LitContext) return create(((LitContext) rc).literal()); - else if (rc instanceof VarContext) { + else if (rc instanceof VarContext) return new Var(((VarContext) rc).ID().getText()); - + else if (rc instanceof EnumContext) { + return enumCreate((EnumContext) rc); } else { return create(((InvocationContext) rc).functionCall()); } @@ -234,6 +238,12 @@ private List getArgs(ArgsContext args) throws LJError { return le; } + private Enum enumCreate(EnumContext enumContext) { + String enumText = enumContext.enumerate().getText(); + String[] parts = enumText.split("\\."); + return new Enum(parts[0], parts[1]); + } + private Expression literalCreate(LiteralContext literalContext) throws LJError { if (literalContext.BOOL() != null) return new LiteralBoolean(literalContext.BOOL().getText()); diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/ExpressionVisitor.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/ExpressionVisitor.java index e3db3938..904690a7 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/ExpressionVisitor.java +++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/visitors/ExpressionVisitor.java @@ -3,6 +3,7 @@ import liquidjava.diagnostics.errors.LJError; import liquidjava.rj_language.ast.AliasInvocation; import liquidjava.rj_language.ast.BinaryExpression; +import liquidjava.rj_language.ast.Enum; import liquidjava.rj_language.ast.FunctionInvocation; import liquidjava.rj_language.ast.GroupExpression; import liquidjava.rj_language.ast.Ite; @@ -40,5 +41,7 @@ public interface ExpressionVisitor { T visitUnaryExpression(UnaryExpression exp) throws LJError; + T visitEnum(Enum en) throws LJError; + T visitVar(Var var) throws LJError; } \ No newline at end of file diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/ExpressionToZ3Visitor.java b/liquidjava-verifier/src/main/java/liquidjava/smt/ExpressionToZ3Visitor.java index 4a45b1d8..8bb46c05 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/smt/ExpressionToZ3Visitor.java +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/ExpressionToZ3Visitor.java @@ -5,6 +5,7 @@ import liquidjava.diagnostics.errors.LJError; import liquidjava.rj_language.ast.AliasInvocation; import liquidjava.rj_language.ast.BinaryExpression; +import liquidjava.rj_language.ast.Enum; import liquidjava.rj_language.ast.FunctionInvocation; import liquidjava.rj_language.ast.GroupExpression; import liquidjava.rj_language.ast.Ite; @@ -120,4 +121,9 @@ public Expr visitUnaryExpression(UnaryExpression exp) throws LJError { default -> null; }; } + + @Override + public Expr visitEnum(Enum en) throws LJError { + return ctx.makeEnum(en.getTypeName(), en.getConstName()); + } } diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java index c660d12b..76a20bea 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorContextToZ3.java @@ -1,11 +1,13 @@ package liquidjava.smt; import com.microsoft.z3.Context; +import com.microsoft.z3.EnumSort; import com.microsoft.z3.Expr; import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; import com.microsoft.z3.Sort; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -13,20 +15,51 @@ import liquidjava.processor.context.GhostFunction; import liquidjava.processor.context.GhostState; import liquidjava.processor.context.RefinedVariable; +import spoon.reflect.declaration.CtEnum; import spoon.reflect.reference.CtTypeReference; public class TranslatorContextToZ3 { static void translateVariables(Context z3, Map> ctx, - Map> varTranslation) { + Map> varTranslation) { + // Translates all variables into z3 expressions, creating EnumSorts once per unique enum type. + Map> enumSorts = new HashMap<>(); - for (String name : ctx.keySet()) - varTranslation.put(name, getExpr(z3, name, ctx.get(name))); + for (Map.Entry> entry : ctx.entrySet()) { + String name = entry.getKey(); + CtTypeReference type = entry.getValue(); + + if (varTranslation.containsKey(name)) continue; + + if (type.isEnum() && type.getDeclaration() instanceof CtEnum enumDecl) { + EnumSort enumSort = translateEnum(z3, varTranslation, enumSorts, type, enumDecl); + // translateEnum may have already registered name as a literal constant + // (e.g. Mode.Photo), no need to overwrite + if (!varTranslation.containsKey(name)) + varTranslation.put(name, z3.mkConst(name, enumSort)); + continue; + } + varTranslation.put(name, getExpr(z3, name, type)); + } varTranslation.put("true", z3.mkBool(true)); varTranslation.put("false", z3.mkBool(false)); } + private static EnumSort translateEnum(Context z3, Map> varTranslation, Map> enumSorts, CtTypeReference type, CtEnum enumDecl) { + // Creates (and caches if needed) a z3 EnumSort for a given enum type. Registers enum literal constants + // on first creation. + return enumSorts.computeIfAbsent(type.getQualifiedName(), k -> { + String[] enumValueNames = enumDecl.getEnumValues().stream() + .map(ev -> ev.getSimpleName()).toArray(String[]::new); + EnumSort enumSort = z3.mkEnumSort(k, enumValueNames); + Expr[] consts = enumSort.getConsts(); + for (int i = 0; i < enumValueNames.length; i++) + varTranslation.put(enumDecl.getSimpleName() + "." + enumValueNames[i], consts[i]); + return enumSort; + }); + } + public static void storeVariablesSubtypes(Context z3, List variables, Map>> varSuperTypes) { for (RefinedVariable v : variables) { @@ -41,6 +74,7 @@ public static void storeVariablesSubtypes(Context z3, List vari private static Expr getExpr(Context z3, String name, CtTypeReference type) { String typeName = type.getQualifiedName(); + return switch (typeName) { case "int", "short", "char", "java.lang.Integer", "java.lang.Short", "java.lang.Character" -> z3 .mkIntConst(name); @@ -88,6 +122,7 @@ static Sort getSort(Context z3, String sort) { case "float", "java.lang.Float" -> z3.mkFPSort32(); case "double", "java.lang.Double" -> z3.mkFPSortDouble(); case "int[]" -> z3.mkArraySort(z3.mkIntSort(), z3.mkIntSort()); + case "java.lang.Enum" -> z3.getIntSort(); case "String" -> z3.getStringSort(); case "void" -> z3.mkUninterpretedSort("void"); default -> z3.mkUninterpretedSort(sort); diff --git a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorToZ3.java b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorToZ3.java index 96f32f66..02fed44a 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorToZ3.java +++ b/liquidjava-verifier/src/main/java/liquidjava/smt/TranslatorToZ3.java @@ -3,6 +3,7 @@ import com.microsoft.z3.ArithExpr; import com.microsoft.z3.ArrayExpr; import com.microsoft.z3.BoolExpr; +import com.microsoft.z3.EnumSort; import com.microsoft.z3.Expr; import com.microsoft.z3.FPExpr; import com.microsoft.z3.FuncDecl; @@ -26,6 +27,7 @@ import liquidjava.diagnostics.errors.NotFoundError; import liquidjava.processor.context.AliasWrapper; import liquidjava.utils.Utils; +import liquidjava.utils.constants.Formats; import liquidjava.utils.constants.Keys; import com.microsoft.z3.enumerations.Z3_sort_kind; @@ -119,6 +121,11 @@ public Expr makeVariable(String name) throws LJError { return expr; } + public Expr makeEnum(String enumTypeName, String enumConstantName) throws LJError { + String varName = String.format(Formats.ENUM, enumTypeName, enumConstantName); + return getVariableTranslation(varName); + } + public Expr makeFunctionInvocation(String name, Expr[] params) throws LJError { if (name.equals("addToIndex")) return makeStore(params); diff --git a/liquidjava-verifier/src/main/java/liquidjava/utils/constants/Formats.java b/liquidjava-verifier/src/main/java/liquidjava/utils/constants/Formats.java index f3ae77f0..7bd8557e 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/utils/constants/Formats.java +++ b/liquidjava-verifier/src/main/java/liquidjava/utils/constants/Formats.java @@ -5,4 +5,5 @@ public final class Formats { public static final String INSTANCE = "#%s_%d"; public static final String THIS = "this#%s"; public static final String RET = "#ret_%d"; + public static final String ENUM = "%s.%s"; } \ No newline at end of file