Skip to content

Commit

Permalink
Merge pull request #60 from fujaba/fix/merge-decls
Browse files Browse the repository at this point in the history
Fix and improve declaration merging
  • Loading branch information
Clashsoft authored Aug 26, 2020
2 parents 57c595b + c6e4048 commit 9374775
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 76 deletions.
6 changes: 3 additions & 3 deletions src/main/antlr/FulibClass.g4
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)?;

Expand Down
162 changes: 115 additions & 47 deletions src/main/java/org/fulib/classmodel/FileFragmentMap.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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<FulibClassParser.ArraySuffixContext> 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 ===============
Expand Down Expand Up @@ -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);
}
Expand Down
64 changes: 56 additions & 8 deletions src/test/java/org/fulib/classmodel/FileFragmentMapTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 23 additions & 18 deletions src/test/java/org/fulib/generator/CustomTemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 */"));
}
}

0 comments on commit 9374775

Please sign in to comment.