PackageInfoGenerator.java

1
package li.selman.nullmarkeder;
2
3
import com.github.javaparser.JavaParser;
4
import com.github.javaparser.ast.CompilationUnit;
5
import com.github.javaparser.ast.NodeList;
6
import com.github.javaparser.ast.expr.AnnotationExpr;
7
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
8
import com.github.javaparser.ast.expr.Name;
9
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
10
import com.google.errorprone.annotations.Var;
11
import java.io.IOException;
12
import java.lang.System.Logger.Level;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.util.List;
17
import java.util.Optional;
18
import java.util.stream.Collectors;
19
import java.util.stream.Stream;
20
21
/**
22
 * Generates or updates {@code package-info.java} files so that every package in a tree is annotated
23
 * with JSpecify's {@code @NullMarked} annotation.
24
 */
25
public final class PackageInfoGenerator {
26
27
    private static final System.Logger LOGGER = System.getLogger(PackageInfoGenerator.class.getName());
28
29
    private static final String DEFAULT_ROOT_DIR = "src/main/java";
30
31
    private static final String REQUIRED_ANNOTATION = "NullMarked";
32
33
    private static final String ANNOTATION_IMPORT = "org.jspecify.annotations.NullMarked";
34
35
    private PackageInfoGenerator() {}
36
37
    public static void updatePackageInfoFiles(String rootDir, String basePackage) throws IOException {
38
        List<String> packages = findPackages(Paths.get(rootDir), basePackage);
39
40
        for (String packageName : packages) {
41
            Path packageInfoPath = Paths.get(rootDir, packageName.replace('.', '/'), "package-info.java");
42
43 1 1. updatePackageInfoFiles : negated conditional → KILLED
            if (Files.exists(packageInfoPath)) {
44 1 1. updatePackageInfoFiles : removed call to li/selman/nullmarkeder/PackageInfoGenerator::updateExistingPackageInfo → KILLED
                updateExistingPackageInfo(packageInfoPath, packageName);
45
            } else {
46 1 1. updatePackageInfoFiles : removed call to li/selman/nullmarkeder/PackageInfoGenerator::generateNewPackageInfo → KILLED
                generateNewPackageInfo(packageInfoPath, packageName);
47
            }
48
        }
49
    }
50
51
    private static void updateExistingPackageInfo(Path packageInfoPath, String packageName) throws IOException {
52
        JavaParser parser = new JavaParser();
53
        String content = Files.readString(packageInfoPath);
54
        Optional<CompilationUnit> result = parser.parse(content).getResult();
55
56 1 1. updateExistingPackageInfo : negated conditional → KILLED
        if (result.isEmpty()) {
57 1 1. updateExistingPackageInfo : removed call to java/lang/System$Logger::log → SURVIVED
            LOGGER.log(Level.WARNING, "Skipping {0}: could not be parsed as valid Java source", packageInfoPath);
58
            return;
59
        }
60
61
        CompilationUnit cu = result.get();
62
        LexicalPreservingPrinter.setup(cu);
63
        @Var boolean modified = false;
64
65 3 1. lambda$updateExistingPackageInfo$0 : replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$0 → SURVIVED
2. updateExistingPackageInfo : negated conditional → KILLED
3. lambda$updateExistingPackageInfo$0 : replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$0 → KILLED
        if (cu.getImports().stream().noneMatch(imp -> imp.getNameAsString().equals(ANNOTATION_IMPORT))) {
66
            cu.addImport(ANNOTATION_IMPORT);
67
            modified = true;
68
        }
69
70 1 1. updateExistingPackageInfo : negated conditional → KILLED
        if (cu.getPackageDeclaration().isEmpty()) {
71
            // A package-info.java with no package declaration has nowhere to attach an
72
            // annotation to; give it one matching its location so the fix below actually sticks.
73
            cu.setPackageDeclaration(packageName);
74
            modified = true;
75
        }
76
        NodeList<AnnotationExpr> annotations =
77
                cu.getPackageDeclaration().orElseThrow().getAnnotations();
78
79
        boolean alreadyAnnotated =
80 2 1. lambda$updateExistingPackageInfo$1 : replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$1 → KILLED
2. lambda$updateExistingPackageInfo$1 : replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$1 → KILLED
                annotations.stream().anyMatch(a -> a.getNameAsString().equals(REQUIRED_ANNOTATION));
81 1 1. updateExistingPackageInfo : negated conditional → KILLED
        if (!alreadyAnnotated) {
82
            annotations.add(new MarkerAnnotationExpr(new Name(REQUIRED_ANNOTATION)));
83
            modified = true;
84
        }
85
86 1 1. updateExistingPackageInfo : negated conditional → KILLED
        if (modified) {
87
            String updatedContent = LexicalPreservingPrinter.print(cu);
88
            Files.writeString(packageInfoPath, updatedContent);
89 1 1. updateExistingPackageInfo : removed call to java/lang/System$Logger::log → SURVIVED
            LOGGER.log(Level.INFO, "Updated {0}", packageInfoPath);
90
        }
91
    }
92
93
    private static void generateNewPackageInfo(Path packageInfoPath, String packageName) throws IOException {
94
        StringBuilder content = new StringBuilder();
95
        content.append("@NullMarked\n");
96
        content.append("package ").append(packageName).append(";\n\n");
97
        content.append("import org.jspecify.annotations.NullMarked;\n");
98
99
        Files.writeString(packageInfoPath, content.toString());
100 1 1. generateNewPackageInfo : removed call to java/lang/System$Logger::log → SURVIVED
        LOGGER.log(Level.INFO, "Generated new package-info.java for {0}", packageName);
101
    }
102
103
    private static List<String> findPackages(Path rootDir, String basePackage) throws IOException {
104
        String basePath = basePackage.replace('.', '/');
105
        Path realRoot = Paths.get(rootDir.toString(), basePath);
106
107
        try (Stream<Path> pathStream = Files.walk(realRoot)) {
108 1 1. findPackages : replaced return value with Collections.emptyList for li/selman/nullmarkeder/PackageInfoGenerator::findPackages → KILLED
            return pathStream
109 2 1. lambda$findPackages$0 : replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$0 → KILLED
2. lambda$findPackages$0 : replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$0 → KILLED
                    .filter(Files::isDirectory)
110 1 1. lambda$findPackages$1 : replaced return value with "" for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$1 → KILLED
                    .map(path -> realRoot.relativize(path).toString())
111
                    // An empty relative path means `path` is realRoot itself - i.e. basePackage - which
112
                    // must be included, not just its subpackages (see PackageInfoGeneratorTest for the
113
                    // regression this covers: this method used to silently skip basePackage entirely).
114 2 1. lambda$findPackages$2 : replaced return value with "" for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$2 → KILLED
2. lambda$findPackages$2 : negated conditional → KILLED
                    .map(relative -> relative.isEmpty() ? basePackage : basePackage + "." + relative.replace('/', '.'))
115
                    .sorted()
116
                    .collect(Collectors.toList());
117
        }
118
    }
119
120
    /**
121
     * Usage: {@code PackageInfoGenerator.main(rootPackage)} or
122
     * {@code PackageInfoGenerator.main(rootPackage, rootDir)}. Defaults {@code rootDir} to
123
     * {@value #DEFAULT_ROOT_DIR}.
124
     */
125
    public static void main(String... args) throws IOException {
126 2 1. main : changed conditional boundary → KILLED
2. main : negated conditional → KILLED
        if (args.length < 1) {
127
            throw new IllegalArgumentException(
128
                    "Usage: PackageInfoGenerator.main(rootPackage) or PackageInfoGenerator.main(rootPackage, rootDir)");
129
        }
130
        String basePackage = args[0];
131 2 1. main : negated conditional → KILLED
2. main : changed conditional boundary → KILLED
        String rootDir = args.length > 1 ? args[1] : DEFAULT_ROOT_DIR;
132 1 1. main : removed call to li/selman/nullmarkeder/PackageInfoGenerator::updatePackageInfoFiles → KILLED
        updatePackageInfoFiles(rootDir, basePackage);
133
    }
134
}

Mutations

43

1.1
Location : updatePackageInfoFiles
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
negated conditional → KILLED

44

1.1
Location : updatePackageInfoFiles
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:addsAnnotationWhenPackageInfoHasNoPackageDeclaration(java.nio.file.Path)]
removed call to li/selman/nullmarkeder/PackageInfoGenerator::updateExistingPackageInfo → KILLED

46

1.1
Location : updatePackageInfoFiles
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
removed call to li/selman/nullmarkeder/PackageInfoGenerator::generateNewPackageInfo → KILLED

56

1.1
Location : updateExistingPackageInfo
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:addsAnnotationWhenPackageInfoHasNoPackageDeclaration(java.nio.file.Path)]
negated conditional → KILLED

57

1.1
Location : updateExistingPackageInfo
Killed by : none
removed call to java/lang/System$Logger::log → SURVIVED
Covering tests

65

1.1
Location : lambda$updateExistingPackageInfo$0
Killed by : none
replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$0 → SURVIVED
Covering tests

2.2
Location : updateExistingPackageInfo
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:addsAnnotationWhenPackageInfoHasNoPackageDeclaration(java.nio.file.Path)]
negated conditional → KILLED

3.3
Location : lambda$updateExistingPackageInfo$0
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:preservesExistingPackageInfoCommentsAndAnnotationsWhenAddingNullMarked(java.nio.file.Path)]
replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$0 → KILLED

70

1.1
Location : updateExistingPackageInfo
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:addsAnnotationWhenPackageInfoHasNoPackageDeclaration(java.nio.file.Path)]
negated conditional → KILLED

80

1.1
Location : lambda$updateExistingPackageInfo$1
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:preservesExistingPackageInfoCommentsAndAnnotationsWhenAddingNullMarked(java.nio.file.Path)]
replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$1 → KILLED

2.2
Location : lambda$updateExistingPackageInfo$1
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:isIdempotentForAlreadyAnnotatedPackage(java.nio.file.Path)]
replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$updateExistingPackageInfo$1 → KILLED

81

1.1
Location : updateExistingPackageInfo
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:isIdempotentForAlreadyAnnotatedPackage(java.nio.file.Path)]
negated conditional → KILLED

86

1.1
Location : updateExistingPackageInfo
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:addsAnnotationWhenPackageInfoHasNoPackageDeclaration(java.nio.file.Path)]
negated conditional → KILLED

89

1.1
Location : updateExistingPackageInfo
Killed by : none
removed call to java/lang/System$Logger::log → SURVIVED
Covering tests

100

1.1
Location : generateNewPackageInfo
Killed by : none
removed call to java/lang/System$Logger::log → SURVIVED
Covering tests

108

1.1
Location : findPackages
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
replaced return value with Collections.emptyList for li/selman/nullmarkeder/PackageInfoGenerator::findPackages → KILLED

109

1.1
Location : lambda$findPackages$0
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
replaced boolean return with false for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$0 → KILLED

2.2
Location : lambda$findPackages$0
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:isIdempotentForAlreadyAnnotatedPackage(java.nio.file.Path)]
replaced boolean return with true for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$0 → KILLED

110

1.1
Location : lambda$findPackages$1
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainUsesProvidedRootDir(java.nio.file.Path)]
replaced return value with "" for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$1 → KILLED

114

1.1
Location : lambda$findPackages$2
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
replaced return value with "" for li/selman/nullmarkeder/PackageInfoGenerator::lambda$findPackages$2 → KILLED

2.2
Location : lambda$findPackages$2
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:generatesPackageInfoForBasePackageItselfWhenItHasNoSubpackages(java.nio.file.Path)]
negated conditional → KILLED

126

1.1
Location : main
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainDefaultsRootDirToSrcMainJava()]
changed conditional boundary → KILLED

2.2
Location : main
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainDefaultsRootDirToSrcMainJava()]
negated conditional → KILLED

131

1.1
Location : main
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainDefaultsRootDirToSrcMainJava()]
negated conditional → KILLED

2.2
Location : main
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainDefaultsRootDirToSrcMainJava()]
changed conditional boundary → KILLED

132

1.1
Location : main
Killed by : li.selman.nullmarkeder.PackageInfoGeneratorTest.[engine:junit-jupiter]/[class:li.selman.nullmarkeder.PackageInfoGeneratorTest]/[method:mainDefaultsRootDirToSrcMainJava()]
removed call to li/selman/nullmarkeder/PackageInfoGenerator::updatePackageInfoFiles → KILLED

Active mutators

Tests examined


Report generated by PIT 1.25.8 support