diff --git a/src/main/antlr/FulibClass.g4 b/src/main/antlr/FulibClass.g4 index 4e54b89c..81e220fa 100644 --- a/src/main/antlr/FulibClass.g4 +++ b/src/main/antlr/FulibClass.g4 @@ -17,8 +17,8 @@ importDecl: IMPORT STATIC? qualifiedName (DOT STAR)? SEMI; classDecl: (modifier | annotation)* classMember; classMember: (CLASS | ENUM | AT? INTERFACE) IDENTIFIER typeParamList? - (EXTENDS annotatedTypeList)? - (IMPLEMENTS annotatedTypeList)? + (EXTENDS extendsTypes=annotatedTypeList)? + (IMPLEMENTS implementsTypes=annotatedTypeList)? classBody; classBody: LBRACE (enumConstants (SEMI (member | SEMI)*)? | (member | SEMI)*) RBRACE; @@ -38,7 +38,7 @@ constructorMember: typeParamList? IDENTIFIER enumConstants: enumConstant (COMMA enumConstant)*; enumConstant: annotation* IDENTIFIER balancedParens? balancedBraces?; -// field: (modifier | annotation)* fieldMember; +field: (modifier | annotation)* fieldMember; fieldMember: type fieldNamePart (COMMA fieldNamePart)* SEMI; fieldNamePart: IDENTIFIER arraySuffix* (EQ expr)?; diff --git a/src/main/java/org/fulib/classmodel/FileFragmentMap.java b/src/main/java/org/fulib/classmodel/FileFragmentMap.java index a584dbee..614275e4 100644 --- a/src/main/java/org/fulib/classmodel/FileFragmentMap.java +++ b/src/main/java/org/fulib/classmodel/FileFragmentMap.java @@ -1,5 +1,11 @@ package org.fulib.classmodel; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.fulib.parser.FulibClassLexer; +import org.fulib.parser.FulibClassParser; + import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; @@ -11,7 +17,6 @@ import java.nio.file.StandardOpenOption; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -49,6 +54,7 @@ public class FileFragmentMap private static final Pattern CLASS_DECL_PATTERN = Pattern.compile("^" + CLASS + "/(\\w+)/" + CLASS_DECL + "$"); private static final Pattern CLASS_END_PATTERN = Pattern.compile("^" + CLASS + "/(\\w+)/" + CLASS_END + "$"); + private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("^" + CLASS + "/(\\w+)/" + ATTRIBUTE + "/(\\w+)$"); private static final String GAP_BEFORE = "#gap-before"; @@ -236,73 +242,135 @@ public boolean classBodyIsEmpty(FileFragmentMap fragmentMap) // package-private for testability static String mergeClassDecl(String oldText, String newText) { - // keep annotations and implements clause "\\s*public\\s+class\\s+(\\w+)(\\.+)\\{" - final Pattern pattern = Pattern.compile("class\\s+(\\w+)\\s*(extends\\s+[^\\s]+)?"); - final Matcher match = pattern.matcher(newText); + // really the only information newText may contain in the current setup is the extends clause + // - the class name is part of the key and will always be identical + // - the visibility is always public and fulib does not allow other modifiers + // - fulib does not allow interfaces + // - fulib does not allow any comments <- TODO this may need to be updated when implementing Javadoc descriptions + final int extendsIndex = newText.indexOf("extends"); + + final CharStream input = CharStreams.fromString(oldText + "}"); + final FulibClassLexer lexer = new FulibClassLexer(input); + final FulibClassParser parser = new FulibClassParser(new CommonTokenStream(lexer)); + final FulibClassParser.ClassDeclContext classDecl = parser.classDecl(); + final FulibClassParser.ClassMemberContext classMember = classDecl.classMember(); - if (!match.find()) + if (extendsIndex < 0) { - // TODO error? - return newText; + if (classMember.EXTENDS() == null) + { + return oldText; + } + + // delete extends clause from oldText + final int startIndex = classMember.EXTENDS().getSymbol().getStartIndex(); + final int endIndex = classMember.IMPLEMENTS() != null ? + classMember.IMPLEMENTS().getSymbol().getStartIndex() : + classMember.classBody().getStart().getStartIndex(); + return new StringBuilder(oldText).delete(startIndex, endIndex).toString(); } - final String className = match.group(1); - final String extendsClause = match.group(2); + final String superType = newText.substring(extendsIndex + "extends".length(), newText.lastIndexOf('{')).trim(); - final int oldClassNamePos = oldText.indexOf("class " + className); - if (oldClassNamePos < 0) + if (classMember.EXTENDS() == null) { - // TODO error? - return newText; + // insert extends clause + final int insertIndex = classMember.IMPLEMENTS() != null ? + classMember.IMPLEMENTS().getSymbol().getStartIndex() : + classMember.classBody().getStart().getStartIndex(); + return new StringBuilder(oldText).insert(insertIndex, "extends " + superType + " ").toString(); } - final StringBuilder newTextBuilder = new StringBuilder(); + // replace super type + final int startIndex = classMember.extendsTypes.getStart().getStartIndex(); + final int endIndex = classMember.extendsTypes.getStop().getStopIndex() + 1; + return new StringBuilder(oldText).replace(startIndex, endIndex, superType).toString(); + } - // prefix - newTextBuilder.append(oldText, 0, oldClassNamePos); + // package-private for testability + // TODO test + static String mergeAttributeDecl(String oldText, String newText) + { + final FulibClassParser.FieldContext oldField = parseField(oldText); + final FulibClassParser.FieldMemberContext oldFieldMember = oldField.fieldMember(); - // middle - newTextBuilder.append("class ").append(className); - if (extendsClause != null) + if (oldFieldMember.fieldNamePart().size() != 1) { - newTextBuilder.append(" ").append(extendsClause); + // oldText is of the form 'int x, y;' or similar - merging that is too complicated + return oldText; } - // suffix - final int implementsPos = oldText.indexOf("implements"); - if (implementsPos >= 0) + final FulibClassParser.FieldNamePartContext oldFieldPart = oldFieldMember.fieldNamePart(0); + + final FulibClassParser.FieldContext newField = parseField(newText); + final FulibClassParser.FieldMemberContext newFieldMember = newField.fieldMember(); + final FulibClassParser.FieldNamePartContext newFieldPart = newFieldMember.fieldNamePart(0); + + // newText provides the following information: + // - type + // - (name) - this is part of the key and will always be identical + // - initializer (optional) + // changes need to be performed from right to left so indices are not messed up + + final StringBuilder builder = new StringBuilder(oldText); + if (newFieldPart.EQ() == null) { - newTextBuilder.append(" ").append(oldText, implementsPos, oldText.length()); + if (oldFieldPart.EQ() != null) + { + // delete everything between the attribute name and the semicolon + final int start = oldFieldPart.IDENTIFIER().getSymbol().getStopIndex() + 1; + final int stop = oldFieldMember.SEMI().getSymbol().getStartIndex(); + builder.delete(start, stop); + } } else { - newTextBuilder.append("\n{"); - } + final FulibClassParser.ExprContext newExpr = newFieldPart.expr(); + final String newExprText = newText.substring(newExpr.getStart().getStartIndex(), + newExpr.getStop().getStopIndex() + 1); - return newTextBuilder.toString(); - } - - // package-private for testability - // TODO test - static String mergeAttributeDecl(String oldText, String newText) - { - // keep everything before public - final int oldPublicPos = oldText.indexOf("public"); - final int newPublicPos = newText.indexOf("public"); - if (oldPublicPos >= 0 && newPublicPos >= 0) - { - return oldText.substring(0, oldPublicPos) + newText.substring(newPublicPos); + if (oldFieldPart.EQ() != null) + { + // replace expr in oldText + final FulibClassParser.ExprContext oldExpr = oldFieldPart.expr(); + final int start = oldExpr.getStart().getStartIndex(); + final int stop = oldExpr.getStop().getStopIndex() + 1; + builder.replace(start, stop, newExprText); + } + else + { + final int insertIndex = oldFieldMember.SEMI().getSymbol().getStartIndex(); + builder.insert(insertIndex, " = " + newExprText); + } } - // keep everything before private - final int newPrivatePos = newText.indexOf("private"); - final int oldPrivatePos = oldText.indexOf("private"); - if (oldPrivatePos >= 0 && newPrivatePos >= 0) + final List arraySuffixes = oldFieldPart.arraySuffix(); + if (!arraySuffixes.isEmpty()) { - return oldText.substring(0, oldPrivatePos) + newText.substring(newPrivatePos); + // delete array suffixes - they can mess with type replacement + final int start = arraySuffixes.get(0).getStart().getStartIndex(); + final int end = arraySuffixes.get(arraySuffixes.size() - 1).getStop().getStopIndex() + 1; + builder.delete(start, end); } - return newText; + // replace old type with new + final FulibClassParser.TypeContext newType = newFieldMember.type(); + final String newTypeText = newText.substring(newType.getStart().getStartIndex(), + newType.getStop().getStopIndex() + 1); + final FulibClassParser.TypeContext oldType = oldFieldMember.type(); + final int start = oldType.getStart().getStartIndex(); + final int stop = oldType.getStop().getStopIndex() + 1; + builder.replace(start, stop, newTypeText); + + return builder.toString(); + } + + private static FulibClassParser.FieldContext parseField(String newText) + { + final CharStream newInput = CharStreams.fromString(newText); + final FulibClassLexer newLexer = new FulibClassLexer(newInput); + final FulibClassParser newParser = new FulibClassParser(new CommonTokenStream(newLexer)); + return newParser.field(); } // =============== Methods =============== @@ -578,11 +646,11 @@ private CodeFragment replace(String key, String newText, int newLines) // newtext contains annotations, thus it overrides annotations in the code // do not modify newtext } - else if (key.equals(CLASS)) + else if (CLASS_DECL_PATTERN.matcher(key).matches()) { newText = mergeClassDecl(oldText, newText); } - else if (key.startsWith(ATTRIBUTE)) + else if (ATTRIBUTE_PATTERN.matcher(key).matches()) { newText = mergeAttributeDecl(oldText, newText); } diff --git a/src/test/java/org/fulib/classmodel/FileFragmentMapTest.java b/src/test/java/org/fulib/classmodel/FileFragmentMapTest.java index 1aaaded5..3336e8e5 100644 --- a/src/test/java/org/fulib/classmodel/FileFragmentMapTest.java +++ b/src/test/java/org/fulib/classmodel/FileFragmentMapTest.java @@ -11,20 +11,68 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertEquals; class FileFragmentMapTest { @Test void mergeClassDecl() { - assertEquals("@Cool class Foo\n{", FileFragmentMap.mergeClassDecl("@Cool class Foo {", "class Foo {")); - assertEquals("@Cool class Foo extends Bar\n{", - FileFragmentMap.mergeClassDecl("@Cool class Foo {", "class Foo extends Bar {")); - assertEquals("class Foo implements Serializable {", - FileFragmentMap.mergeClassDecl("class Foo implements Serializable {", "class Foo {")); - assertEquals("class Foo extends Bar implements Baz {", - FileFragmentMap.mergeClassDecl("class Foo implements Baz {", "class Foo extends Bar {")); + assertThat("it keeps annotations", FileFragmentMap.mergeClassDecl("@Cool class Foo {", "class Foo {"), + equalTo("@Cool class Foo {")); + assertThat("it keeps interfaces", + FileFragmentMap.mergeClassDecl("class Foo implements Serializable {", "class Foo {"), + equalTo("class Foo implements Serializable {")); + + assertThat("it removes extends clause", FileFragmentMap.mergeClassDecl("class Foo extends Bar {", "class Foo {"), + equalTo("class Foo {")); + assertThat("it removes extends clause and keeps interfaces", + FileFragmentMap.mergeClassDecl("class Foo extends Bar implements Baz {", "class Foo {"), + equalTo("class Foo implements Baz {")); + + assertThat("it adds extends clause before interface", + FileFragmentMap.mergeClassDecl("class Foo implements Baz {", "class Foo extends Bar {"), + equalTo("class Foo extends Bar implements Baz {")); + assertThat("it adds extends clause before body", + FileFragmentMap.mergeClassDecl("class Foo {", "class Foo extends Bar {"), + equalTo("class Foo extends Bar {")); + assertThat("it adds extends clause and keeps annotations", + FileFragmentMap.mergeClassDecl("@Cool class Foo {", "class Foo extends Bar {"), + equalTo("@Cool class Foo extends Bar {")); + + assertThat("it replaces the super type", + FileFragmentMap.mergeClassDecl("class Foo extends Moo {", "class Foo extends Bar {"), + equalTo("class Foo extends Bar {")); + assertThat("it replaces the super type and keeps interfaces", + FileFragmentMap.mergeClassDecl("class Foo extends Moo implements Baz {", "class Foo extends Bar {"), + equalTo("class Foo extends Bar implements Baz {")); + } + + @Test + void mergeAttributeDecl() + { + assertThat("it ignores multi-declarations", FileFragmentMap.mergeAttributeDecl("int x, y;", "long x;"), + equalTo("int x, y;")); + + assertThat("it removes the initializer", FileFragmentMap.mergeAttributeDecl("int x = 0;", "int x;"), + equalTo("int x;")); + assertThat("it removes the initializer and keeps annotations", + FileFragmentMap.mergeAttributeDecl("@Cool int x = 0;", "int x;"), equalTo("@Cool int x;")); + + assertThat("it replaces the initializer", FileFragmentMap.mergeAttributeDecl("int x = 0;", "int x = 1;"), + equalTo("int x = 1;")); + assertThat("it replaces the initializer and keeps annotations", + FileFragmentMap.mergeAttributeDecl("@Cool int x = 0;", "int x = 1;"), equalTo("@Cool int x = 1;")); + + assertThat("it adds the initializer", FileFragmentMap.mergeAttributeDecl("int x;", "int x = 1;"), + equalTo("int x = 1;")); + assertThat("it adds the initializer and keeps annotations", + FileFragmentMap.mergeAttributeDecl("@Cool int x;", "int x = 1;"), equalTo("@Cool int x = 1;")); + + assertThat("it updates the type", FileFragmentMap.mergeAttributeDecl("int x;", "long x;"), equalTo("long x;")); + assertThat("it updates the type and keeps annotations", + FileFragmentMap.mergeAttributeDecl("@Cool int x;", "long x;"), equalTo("@Cool long x;")); + assertThat("it updates the type and removes C-style arrays", + FileFragmentMap.mergeAttributeDecl("int x[];", "long x;"), equalTo("long x;")); } @Test diff --git a/src/test/java/org/fulib/generator/CustomTemplateTest.java b/src/test/java/org/fulib/generator/CustomTemplateTest.java index e0c8c209..aad4bf7c 100644 --- a/src/test/java/org/fulib/generator/CustomTemplateTest.java +++ b/src/test/java/org/fulib/generator/CustomTemplateTest.java @@ -8,11 +8,12 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -27,33 +28,37 @@ void testCustomTemplates() throws IOException Tools.removeDirAndFiles(targetFolder); - ClassModelBuilder mb = Fulib.classModelBuilder("org.fulib.studyright", srcFolder); + final ClassModelBuilder mb = Fulib.classModelBuilder("org.fulib.studyright", srcFolder); mb.buildClass("University").buildAttribute("name", Type.STRING); - mb.buildClass("Student").buildAttribute("name", Type.STRING, "\"Karli\"") - .buildAttribute("matrNo", Type.LONG, "0"); + mb + .buildClass("Student") + .buildAttribute("name", Type.STRING, "\"Karli\"") + .buildAttribute("matrNo", Type.LONG, "0"); - ClassModel model = mb.getClassModel(); - - // generate normal - Fulib.generator().generate(model); - - byte[] bytes = Files.readAllBytes(Paths.get(model.getPackageSrcFolder() + "/Student.java")); - String content = new String(bytes); - assertThat(content, not(containsString("/* custom attribute comment */"))); - - int returnCode = Tools.javac(outFolder, model.getPackageSrcFolder()); - assertThat("compiler return code: ", returnCode, is(0)); + final ClassModel model = mb.getClassModel(); // generate custom // start_code_fragment: testCustomTemplates Fulib.generator().setCustomTemplatesFile("templates/custom.stg").generate(model); // end_code_fragment: - bytes = Files.readAllBytes(Paths.get(model.getPackageSrcFolder() + "/Student.java")); - content = new String(bytes); + final Path path = Paths.get(model.getPackageSrcFolder(), "Student.java"); + final String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); assertThat(content, containsString("/* custom attribute comment */")); - returnCode = Tools.javac(outFolder, model.getPackageSrcFolder()); + final int returnCode = Tools.javac(outFolder, model.getPackageSrcFolder()); assertThat("compiler return code: ", returnCode, is(0)); + + // change comment text + final String userChangedContent = content.replace("/* custom attribute comment */", + "/* attribute comment changed by user */"); + Files.write(path, userChangedContent.getBytes(StandardCharsets.UTF_8)); + + // generate custom again + Fulib.generator().setCustomTemplatesFile("templates/custom.stg").generate(model); + + final String loadedContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + assertThat("it keeps user changes intact", loadedContent, + containsString("/* attribute comment changed by user */")); } }