Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix Ordering issue in PBJ Comparable handling #189

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/zxc-compile-pbj-code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ on:
description: "Java JDK Version:"
type: string
required: false
default: "17.0.9"
default: "21.0.2"
gradle-version:
description: "Gradle Version:"
type: string
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,9 @@ Temporary Items

### Generated Protobuf Files
/tests/src/main/proto/

### IntelliJ workarounds
.github/.github.iml
github-setup


Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ plugins {
group = "com.hedera.pbj"

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
languageVersion.set(JavaLanguageVersion.of(21))
vendor.set(JvmVendorSpec.ADOPTIUM)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
Expand All @@ -20,11 +25,6 @@
public final class Common {
/** The indent for fields, default 4 spaces */
public static final String FIELD_INDENT = " ".repeat(4);

public static final int DEFAULT_INDENT = 4;
/** Number of bits used to represent the tag type */
static final int TAG_TYPE_BITS = 3;

/** Wire format code for var int */
public static final int TYPE_VARINT = 0;
/** Wire format code for fixed 64bit number */
Expand All @@ -33,8 +33,20 @@
public static final int TYPE_LENGTH_DELIMITED = 2;
/** Wire format code for fixed 32bit number */
public static final int TYPE_FIXED32 = 5;
public static final int DEFAULT_INDENT = 4;

Check notice on line 36 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L36

'VARIABLE_DEF' should be separated from previous line.

/** Number of bits used to represent the tag type */
static final int TAG_TYPE_BITS = 3;

private static final Pattern COMPARABLE_PATTERN = Pattern.compile("[)] implements Comparable<\\w+> [{]");
private static final String FIELD_NOT_IMPLEMENT_COMPARABLE_MESSAGE =

Check notice on line 42 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L42

'VARIABLE_DEF' should be separated from previous line.
"Field %s.%s specified in `pbj.comparable` option must implement `Comparable` interface but it doesn't.";
private static final String JAVA_FILE_PATH_FORM = "%1$s%2$c%1$s".formatted("%s", File.separatorChar);

Check notice on line 44 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L44

'VARIABLE_DEF' should be separated from previous line.
private static final String JAVA_FILE_MISSING_MESSAGE =

Check notice on line 45 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L45

'VARIABLE_DEF' should be separated from previous line.
"Unable to read Java file %1$s for class %2$s in package %3$s under source root %4$s.";
private static final String ESCAPED_FILE_SEPARATOR = "\\" + File.separator;

Check notice on line 47 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L47

'VARIABLE_DEF' should be separated from previous line.

private Common() {}

/**
* Makes a tag value given a field number and wire type.
Expand Down Expand Up @@ -257,7 +269,7 @@
}
""").replace("$fieldName", f.nameCamelFirstLower());
} else {
throw new RuntimeException("Unexpected field type for getting HashCode - " + f.type().toString());
throw new RuntimeException("Unexpected field type for getting HashCode - " + f.type());

Check warning on line 272 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L272

Avoid throwing raw exception types.
}
}
}
Expand Down Expand Up @@ -422,7 +434,7 @@
}
""").replace("$fieldName", f.nameCamelFirstLower());
} else {
throw new IllegalArgumentException("Unexpected field type for getting Equals - " + f.type().toString());
throw new IllegalArgumentException("Unexpected field type for getting Equals - " + f.type());
}
}
}
Expand All @@ -449,7 +461,6 @@
}
""").replace("$fieldName", f.nameCamelFirstLower());
case "BoolValue" ->

generatedCodeSoFar += (
"""
if (this.$fieldName == null && thatObj.$fieldName != null) {
Expand Down Expand Up @@ -481,7 +492,6 @@
""").replace("$fieldName", f.nameCamelFirstLower());
default -> throw new UnsupportedOperationException("Unhandled optional message type:" + f.messageType());
}
;
return generatedCodeSoFar;
}

Expand Down Expand Up @@ -593,7 +603,7 @@
verifyComparable(f, destinationSrcDir);
generatedCodeSoFar += generateCompareToForObject(f);
} else {
throw new IllegalArgumentException("Unexpected field type for getting CompareTo - " + f.type().toString());
throw new IllegalArgumentException("Unexpected field type for getting CompareTo - " + f.type());

Check notice on line 606 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L606

Line is longer than 120 characters (found 136).
}
}
}
Expand Down Expand Up @@ -626,24 +636,27 @@
private static void verifyComparable(final Field field, File destinationSrcDir) {
if (field instanceof final SingleField singleField) {
if (singleField.type() != Field.FieldType.MESSAGE) {
// everything else except message and bytes is comparable for sure
// everything except message and repeated is comparable for sure
return;
}
// let's check if the message implements Comparable
final String className = singleField.javaFieldType();
final File javaFile = getJavaFile(destinationSrcDir, singleField.messageTypeModelPackage(), className);
try (BufferedReader reader = new BufferedReader(new FileReader(javaFile))) {
String line;
while ((line = reader.readLine()) != null) {
if (COMPARABLE_PATTERN.matcher(line).matches()) {
return;
}
}
throw new IllegalArgumentException(("Field %s.%s specified in `pbj.comparable` option must implement " +
"`Comparable` interface but it doesn't.").formatted(className, field.nameCamelFirstLower()));
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
final File javaFile =
getJavaFile(destinationSrcDir, singleField.messageTypeModelPackage(), className, true);
try (BufferedReader reader = new BufferedReader(new FileReader(javaFile))) {
String line;
while ((line = reader.readLine()) != null) {
if (COMPARABLE_PATTERN.matcher(line).matches()) {
return;
}
}
throw new IllegalArgumentException(
FIELD_NOT_IMPLEMENT_COMPARABLE_MESSAGE.formatted(className, field.nameCamelFirstLower()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} if (field instanceof final OneOfField oneOfField) {
oneOfField.fields().forEach(v -> verifyComparable(v, destinationSrcDir));
} else {
Expand Down Expand Up @@ -696,25 +709,97 @@
* @return text without a leading dot
*/
public static String removingLeadingDot(String text) {
if (!text.isEmpty() & text.charAt(0) == '.') {
if (!text.isEmpty() && text.charAt(0) == '.') {
return text.substring(1);
}
return text;
}

/**
/**
* Get the java file for a src directory, package and classname with optional suffix. All parent directories will
* also be created.
*
* @param srcDir The src dir root of all java src
* @param sourceFolder The source folder root of all generated java sources
* @param javaPackage the java package with '.' deliminators
* @param className the camel case class name
* @param className the proper camel case class name
* @return File object for java file
* @throws IOException if the source folder is invalid.
*/
public static File getJavaFile(File srcDir, String javaPackage, String className) {
File packagePath = new File(srcDir.getPath() + File.separatorChar + javaPackage.replaceAll("\\.","\\" + File.separator));
//noinspection ResultOfMethodCallIgnored
packagePath.mkdirs();
return new File(packagePath,className+".java");
public static File getJavaFile(final File sourceFolder, final String javaPackage, final String className)
throws IOException {
return getJavaFile(sourceFolder, javaPackage, className, false);
}

/**
* Get the java file for a src directory, package and classname with optional suffix. All parent directories will
* also be created.
*
* @param sourceFolder The source folder root of all generated java sources
* @param javaPackage the java package with '.' deliminators
* @param className the proper camel case class name
* @param mustExist if true, then the file to return must already exist.
* @return File object for java file
* @throws IOException if the source folder is invalid,
* or the java file requested does not exist and mustExist is true.
*/
public static File getJavaFile(final File sourceFolder, final String javaPackage, final String className,
final boolean mustExist) throws IOException {
final String sourcePath = sourceFolder.getCanonicalPath();
final File packagePath =
new File(JAVA_FILE_PATH_FORM.formatted(sourcePath, javaPackage.replaceAll("\\.", ESCAPED_FILE_SEPARATOR)));

Check notice on line 749 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L749

Line is longer than 120 characters (found 123).
if (!packagePath.exists()) packagePath.mkdirs();
final File javaFile = new File(packagePath, className + ".java");
if (mustExist && (!javaFile.exists() || !javaFile.canRead())) {
listFolder(javaFile.getParentFile());
final String message =
JAVA_FILE_MISSING_MESSAGE.formatted(javaFile.getCanonicalPath(), className, javaPackage, sourcePath);

Check notice on line 755 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L755

Line is longer than 120 characters (found 121).
throw new IOException(message);
}
return javaFile;
}

private static void listFolder(final File folderToList) throws IOException {
System.err.println("Files in Target Folder:");
Files.walkFileTree(folderToList.toPath(), new PrintVisitor());
}

private static class PrintVisitor extends SimpleFileVisitor<Path> {
private static final String BASE_INDENT = " ";
private int depth = 1;

Check notice on line 768 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L768

'VARIABLE_DEF' should be separated from previous line.
private String indent = BASE_INDENT;

Check notice on line 769 in pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Common.java#L769

'VARIABLE_DEF' should be separated from previous line.

@Override
public FileVisitResult preVisitDirectory(Path folder, BasicFileAttributes attributes) {
System.err.print(indent);
System.err.print(folder.toFile().getName());
System.err.println(File.separator);
depth++;
indent += BASE_INDENT;
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path folder, IOException exc) throws IOException {
if (exc != null) {
throw exc;
} else {
depth--;
if (depth > 1) {
indent = indent.substring(0, (depth * BASE_INDENT.length()) - 1);
}
else {
indent = BASE_INDENT;
depth = 1;
}
return FileVisitResult.CONTINUE;
}
}

@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
System.err.print(indent);
System.err.println(file.toFile().getName());
return FileVisitResult.CONTINUE;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.hedera.pbj.compiler.impl;

import static com.hedera.pbj.compiler.impl.Common.TYPE_FIXED32;
import static com.hedera.pbj.compiler.impl.Common.TYPE_FIXED64;
import static com.hedera.pbj.compiler.impl.Common.TYPE_LENGTH_DELIMITED;
import static com.hedera.pbj.compiler.impl.Common.TYPE_VARINT;
import static com.hedera.pbj.compiler.impl.Common.snakeToCamel;

import com.hedera.pbj.compiler.impl.grammar.Protobuf3Parser;
import edu.umd.cs.findbugs.annotations.NonNull;

import java.util.Set;

import static com.hedera.pbj.compiler.impl.Common.*;

/**
* Interface for SingleFields and OneOfFields
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.hedera.pbj.test.proto.pbj.Hasheval;
Expand Down Expand Up @@ -152,4 +152,4 @@ public void benchJavaRecordNotEquals(Blackhole blackhole) {
blackhole.consume(hashevalJavaRecord.equals(hashevalJavaRecordDifferent));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.hedera.pbj.test.proto.pbj.TimestampTest;
import org.openjdk.jmh.annotations.Benchmark;
Expand Down Expand Up @@ -102,4 +102,4 @@ public void benchJavaRecordNotEquals(Blackhole blackhole) {
blackhole.consume(timestampStandardRecord.equals(timestampStandardRecordDifferent));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.hedera.pbj.intergration.test.TestHashFunctions;
import com.hedera.pbj.integration.test.TestHashFunctions;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.hedera.pbj.test.proto.pbj.Hasheval;
import com.hedera.pbj.test.proto.pbj.Suit;
Expand Down Expand Up @@ -53,4 +53,4 @@ public void hashBenchFieldWise(Blackhole blackhole) throws IOException {
TestHashFunctions.hash2(hasheval);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.google.protobuf.GeneratedMessageV3;
import com.google.protobuf.InvalidProtocolBufferException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.GeneratedMessageV3;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.hedera.pbj.intergration.jmh;
package com.hedera.pbj.integration.jmh;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
Expand Down
23 changes: 23 additions & 0 deletions pbj-integration-tests/src/main/proto/comparable.proto
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,28 @@ message LimitedComparableTestWithOneOf {
string oneOfText = 10;
ComparableSubObj oneOfSubObject = 12;
}
}

/**
* Help to encounter the error found in issue #180 by providing an enum with a name
* that has unusual "almost" camel case. The odd case of "ENum" is deliberate here.
*/
enum OddCaseENum {
UP = 0;
DOWN = 1;
CHARM = 2;
STRANGE = 3;
TOP = 4;
BOTTOM = 5;
}

/**
* This exercises the error found in issue #180 by including fields of the
* problematic message types.
*/
// <<<pbj.comparable = "test_comparable_value, test_enum_value, simple_stringValue" >>>
message ComparableContainsWrongCase {
ComparableWrongCaseID test_comparable_value = 1;
OddCaseENum test_enum_value = 2;
string simple_stringValue = 3;
}
13 changes: 12 additions & 1 deletion pbj-integration-tests/src/main/proto/everything.proto
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,15 @@ message InnerEverything {
google.protobuf.BytesValue bytesBoxedOneOf = 100025;
google.protobuf.StringValue stringBoxedOneOf = 100026;
}
}
}

/**
* Example of a message which is named <strong>almost</strong> like a Java class, but not quite.
* This helps test the error originally found in issue #180.
* Placed here to ensure comparable works across imported files.
*/
// <<<pbj.comparable = "subMessageCMP, sub_message_two" >>>
message ComparableWrongCaseID {
TimestampTest subMessageCMP = 1;
Suit sub_message_two = 2;
}
Loading
Loading