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

NPE when formatting java file with java 21 preview features. #937

Closed
sureshg opened this issue May 2, 2023 · 4 comments · Fixed by #999
Closed

NPE when formatting java file with java 21 preview features. #937

sureshg opened this issue May 2, 2023 · 4 comments · Fixed by #999
Labels

Comments

@sureshg
Copy link

sureshg commented May 2, 2023

Env

$ openjdk 21-ea 2023-09-19
OpenJDK Runtime Environment (build 21-ea+20-1677)
OpenJDK 64-Bit Server VM (build 21-ea+20-1677, mixed mode, sharing)

google-javaformat              = "1.17.0"

Error

> Task :spotlessJava FAILED
Step 'google-java-format' found problem in 'src/main/java/xxx/DOP.java':
57:10: error: java.lang.NullPointerException: Cannot invoke "com.sun.tools.javac.tree.JCTree.getStartPosition()" because "node" is null
        at com.google.googlejavaformat.java.JavaInputAstVisitor.sync(JavaInputAstVisitor.java:3933)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitToDeclare(JavaInputAstVisitor.java:2799)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitEnhancedForLoop(JavaInputAstVisitor.java:791)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitEnhancedForLoop(JavaInputAstVisitor.java:167)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCEnhancedForLoop.accept(JCTree.java:1248)
        at jdk.compiler/com.sun.source.util.TreePathScanner.scan(TreePathScanner.java:92)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.scan(JavaInputAstVisitor.java:357)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitStatements(JavaInputAstVisitor.java:2229)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.methodBody(JavaInputAstVisitor.java:1550)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitMethod(JavaInputAstVisitor.java:1537)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitMethod(JavaInputAstVisitor.java:167)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCMethodDecl.accept(JCTree.java:944)
        at jdk.compiler/com.sun.source.util.TreePathScanner.scan(TreePathScanner.java:92)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.scan(JavaInputAstVisitor.java:357)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.addBodyDeclarations(JavaInputAstVisitor.java:3760)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.visitClassDeclaration(JavaInputAstVisitor.java:2044)
        at com.google.googlejavaformat.java.java17.Java17InputAstVisitor.visitClass(Java17InputAstVisitor.java:120)
        at com.google.googlejavaformat.java.java17.Java17InputAstVisitor.visitClass(Java17InputAstVisitor.java:51)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCClassDecl.accept(JCTree.java:851)
        at jdk.compiler/com.sun.source.util.TreePathScanner.scan(TreePathScanner.java:92)
        at com.google.googlejavaformat.java.JavaInputAstVisitor.scan(JavaInputAstVisitor.java:357)

I am not sure for which part it's failing. I was playing the java 21 preview features. So here is the sample file used in the task

Sample

import org.jspecify.annotations.NullMarked;

import java.io.*;
import java.lang.reflect.RecordComponent;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static java.lang.System.out;
import static java.util.Objects.requireNonNull;

public class DOP {

    public static void main(String[] args) throws Exception {
        run();
    }

    public static void run() throws Exception {
        @NullMarked
        record Person(String name, int age) {
        }

        var future = new CompletableFuture<>();
        var textBlock = """
                This is text block
                This will join \
                with the line : %s
                "quote" = "added"
                Escape Start  \n \t \r \b \f end
                Space Escape-\s\s\s\s\s\s\s\s\s\s-end
                Regex \\S \\d \\D \\w \\W
                \\d+
                Escape char: \u0020 \u00A0 \u2000 \u3000 \uFEFF \u200B \u200C \u200D \u2028 \u2029
                END
                """
                .formatted(new Person("Foo", 40));
        future.complete(textBlock);
        out.println(future.get());

        amberReflections();
        recordPatterns();
        genericRecordPattern();
        serializeRecord();
    }

    private static void recordPatterns() {
        record Point(int x, int y) {
        }
        var points = List.of(new Point(1, 2), new Point(3, 4), new Point(5, 6));
        // Record pattern in enhanced for loop
        for (Point(var x, var y) : points) {
            out.println("Point: (" + x + ", " + y + ")");
        }
    }

    interface Name<T> {
    }

    record FullName<T>(T firstName, T lastName) implements Name<T> {
    }

    private static void print(Name name) {
        var result = switch (name) {
            case FullName(var first, var last) -> first + ", " + last;
            default -> "Invalid name";
        };
        out.println(result);

        if (name instanceof FullName<?> f) {
            // out.println(f.firstName() + ", " + f.lastName());
        }

        // Named record pattern is not supported
        if (name instanceof FullName(var first, var last)) {
            // out.println(first + ", " + last);
        }
    }

    private static void genericRecordPattern() {
        print(new FullName<>("Foo", "Bar"));
        print(new FullName<>(1, 2));
        print(new FullName<>(10L, 20L));
    }

    private static void amberReflections() {
        var sealedClazz = Result.class;
        out.println("Result (Interface)  -> " + sealedClazz.isInterface());
        out.println("Result (Sealed Class) -> " + sealedClazz.isSealed());

        for (Class<?> permittedSubclass : sealedClazz.getPermittedSubclasses()) {
            out.println("\nPermitted Subclass : " + permittedSubclass.getName());
            if (permittedSubclass.isRecord()) {
                out.println(permittedSubclass.getSimpleName() + " record components are,");
                for (RecordComponent rc : permittedSubclass.getRecordComponents()) {
                    out.println(rc);
                }
            }
        }
    }


    private static void serializeRecord() throws Exception {
        // Local record
        record Lang(String name, int year) implements Serializable {

            Lang {
                requireNonNull(name);
                if (year <= 0) {
                    throw new IllegalArgumentException("Invalid year " + year);
                }
            }
        }

        var serialFile = Files.createTempFile("record-serial", "data").toFile();
        serialFile.deleteOnExit();

        try (var oos = new ObjectOutputStream(new FileOutputStream(serialFile))) {
            List<Record> recs = List.of(
                    new Lang("Java", 25),
                    new Lang("Kotlin", 10),
                    (Record) Result.success(100)
            );

            for (Record rec : recs) {
                out.println("Serializing record: " + rec);
                oos.writeObject(rec);
            }
            oos.writeObject(null); // EOF
        }

        try (var ois = new ObjectInputStream(new FileInputStream(serialFile))) {
            Object rec;
            while ((rec = ois.readObject()) != null) {
                var result = switch (rec) {
                    case null -> "n/a";
                    case Lang l when l.year >= 20 -> l.toString();
                    case Lang(var name, var year) -> name;
                    case Result<?> r -> "Result value: " + r.getOrNull();
                    default -> "Invalid serialized data. Expected Result, but found " + rec;
                };

                out.println("Deserialized record: " + rec);
                out.println(result);
            }
        }

        results().forEach(r -> {
            var result = switch (r) {
                case null -> "n/a";
                case Result.Success<?> s -> s.toString();
                case Result.Failure<?> f -> f.toString();
            };
            out.println("Result (Sealed Type): " + result);
        });
    }

    static List<Result<?>> results() {
        return Arrays.asList(getResult(5), getResult(25), getResult(-1));
    }

    static Result<Number> getResult(long l) {
        // Unnecessary boxing required for boolean check in switch expression
        return switch (Long.valueOf(l)) {
            case Long s when s > 0 && s < 10 -> Result.success(s);
            case Long s when s > 10 -> Result.failure(new IllegalArgumentException(String.valueOf(s)));
            default -> null;
        };
    }
}
@sormuras
Copy link
Contributor

sormuras commented May 2, 2023

``@sureshg mind that JEP 440 removes the support for record patterns in enhanced for-loops.

History
Record patterns were proposed as a preview feature by JEP 405 and delivered in JDK 19, and previewed a second time by JEP 432 and delivered in JDK 20.
[...]
Apart from some minor editorial changes, the main change since the second preview is to remove support for record patterns appearing in the header of an enhanced for statement. This feature may be re-proposed in a future JEP.

My guess is that your JDK 21-ea version already dropped the support and therefore jdk.compiler/com.sun.tools.javac.tree.JCTree$JCEnhancedForLoop.accept(...) stumbles.

Your code uses such a for-loop here:

        // Record pattern in enhanced for loop
        for (Point(var x, var y) : points) {
            out.println("Point: (" + x + ", " + y + ")");
        }

@sureshg
Copy link
Author

sureshg commented May 2, 2023

@sormuras thanks, i missed that part. I am using the latest ea build 21-ea+20 . The thing is, it still compiles and run from command line. The issue is only when running the formatting task.

...
Point: (1, 2)
Point: (3, 4)
Point: (5, 6)

@sureshg
Copy link
Author

sureshg commented May 2, 2023

Removing record pattern from for loop causes another error

67:32: error: did not generate token "("
com.google.googlejavaformat.java.FormatterException: 67:32: error: did not generate token "("
        at com.google.googlejavaformat.java.Formatter.getFormatReplacements(Formatter.java:275)
        at com.google.googlejavaformat.java.Formatter.formatSource(Formatter.java:247)
        at com.google.googlejavaformat.java.Formatter.formatSource(Formatter.java:213)
        at com.diffplug.spotless.glue.java.GoogleJavaFormatFormatterFunc.apply(GoogleJavaFormatFormatterFunc.java:57)
        at com.diffplug.spotless.FormatterFunc.apply(FormatterFunc.java:32)
        at com.diffplug.spotless.FormatterStepImpl$Standard.format(FormatterStepImpl.java:82)

@cushon
Copy link
Collaborator

cushon commented Dec 7, 2023

67:32: error: did not generate token "("

Pattern matching in switch isn't supported yet, I'm working on fixing that part:

 class T {
   {
     var result = switch (name) {
     case FullName(var first, var last) -> first + ", " + last;
     default -> "Invalid name";
    };
  }
}

@cushon cushon added the Java 21 label Dec 7, 2023
copybara-service bot pushed a commit that referenced this issue Dec 8, 2023
Fixes #937, #880

PiperOrigin-RevId: 588882242
copybara-service bot pushed a commit that referenced this issue Dec 8, 2023
Fixes #937, #880

PiperOrigin-RevId: 588882242
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants