Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
114 changes: 114 additions & 0 deletions docs/jte-nullmarked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: jte-nullmarked NullMarked annotation generator
description: A generator extension for jte that adds @NullMarked package annotations to generated classes.
---

# jte-nullmarked NullMarked Annotation Generator

jte-nullmarked is a generator extension for jte that creates `package-info.java` files annotated
with `@NullMarked` for each package containing generated classes. This enables null-safety tooling
(such as NullAway or ErrorProne) to treat generated classes as null-safe, preventing build failures
in projects that enforce null-safety annotations.

## Setup

Add `jspecify` 1.0.0 or later to your project dependencies, then configure the build plugin to use the extension.

=== "Maven"

Add to your `<dependencies>` section:

```xml linenums="1"
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
```

Add to your `<build><plugins>` section:

```xml linenums="1"
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>${jte.version}</version>
<configuration>
<sourceDirectory>${project.basedir}/src/main/jte</sourceDirectory>
<contentType>Html</contentType>
<extensions>
<extension>
<className>gg.jte.nullmarked.NullMarkedExtension</className>
</extension>
</extensions>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-nullmarked</artifactId>
<version>${jte.version}</version>
</dependency>
</dependencies>
</plugin>
```

=== "Gradle (Groovy DSL)"

```groovy linenums="1"
plugins {
id 'gg.jte.gradle' version '${jte.version}'
}

dependencies {
implementation 'gg.jte:jte-runtime:${jte.version}'
implementation 'org.jspecify:jspecify:1.0.0'
jteGenerate 'gg.jte:jte-nullmarked:${jte.version}'
}

jte {
generate()
jteExtension 'gg.jte.nullmarked.NullMarkedExtension'
}
```

=== "Gradle (Kotlin DSL)"

```kotlin linenums="1"
plugins {
id("gg.jte.gradle") version "${jte.version}"
}

dependencies {
implementation("gg.jte:jte-runtime:${jte.version}")
implementation("org.jspecify:jspecify:1.0.0")
jteGenerate("gg.jte:jte-nullmarked:${jte.version}")
}

jte {
generate()
jteExtension("gg.jte.nullmarked.NullMarkedExtension")
}
```

Run the build to generate classes.

## Output

For each package containing generated classes, a `package-info.java` file is created:

```java
@NullMarked
package gg.jte.generated.precompiled;

import org.jspecify.annotations.NullMarked;
```

If a `package-info.java` already exists in a package directory, it is left unchanged.
3 changes: 3 additions & 0 deletions jte-nullmarked/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# jte-nullmarked

See official docs: <https://jte.gg/jte-nullmarked/>.
46 changes: 46 additions & 0 deletions jte-nullmarked/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>gg.jte</groupId>
<artifactId>jte-parent</artifactId>
<version>3.2.4-SNAPSHOT</version>
</parent>

<artifactId>jte-nullmarked</artifactId>
Comment thread
kelunik marked this conversation as resolved.
Outdated
<name>jte-nullmarked</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-extension-api</artifactId>
<version>3.2.4-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-extension-api-mocks</artifactId>
<version>3.2.4-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>gg.jte.nullmarked</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package gg.jte.nullmarked;

import gg.jte.extension.api.JteConfig;
import gg.jte.extension.api.JteExtension;
import gg.jte.extension.api.TemplateDescription;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

@SuppressWarnings("unused")
public class NullMarkedExtension implements JteExtension {

@Override
public String name() {
return "NullMarked package-info generator";
}

@Override
public Collection<Path> generate(JteConfig config, Set<TemplateDescription> templateDescriptions) {
if (config.generatedSourcesRoot() == null || templateDescriptions.isEmpty()) {
return Collections.emptyList();
}

return templateDescriptions.stream()
.map(TemplateDescription::packageName)
.distinct()
.map(pkg -> writePackageInfo(config.generatedSourcesRoot(), pkg))
.collect(Collectors.toList());
}

private Path writePackageInfo(Path sourcesRoot, String packageName) {
Path packageDir = sourcesRoot.resolve(packageName.replace('.', '/'));
Path file = packageDir.resolve("package-info.java");
if (Files.exists(file)) {
return file;
}
try {
Files.createDirectories(packageDir);
Files.writeString(file,
"@NullMarked\npackage " + packageName + ";\n\nimport org.jspecify.annotations.NullMarked;\n");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return file;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package gg.jte.nullmarked;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Set;

import static gg.jte.extension.api.mocks.MockConfig.mockConfig;
import static gg.jte.extension.api.mocks.MockTemplateDescription.mockTemplateDescription;
import static org.assertj.core.api.Assertions.assertThat;

class NullMarkedExtensionTest {

private final NullMarkedExtension extension = new NullMarkedExtension();

@TempDir
Path tempDir;

@Test
void singlePackage() throws IOException {
var config = mockConfig().generatedSourcesRoot(tempDir).packageName("com.example");
var template = mockTemplateDescription().packageName("com.example").className("JtefooGenerated").name("foo.jte");

Collection<Path> result = extension.generate(config, Set.of(template));

assertThat(result).hasSize(1);
Path packageInfo = tempDir.resolve("com/example/package-info.java");
assertThat(packageInfo).exists();
assertThat(Files.readString(packageInfo)).isEqualTo(
"@NullMarked\npackage com.example;\n\nimport org.jspecify.annotations.NullMarked;\n");
}

@Test
void multipleTemplatesSamePackage() {
var config = mockConfig().generatedSourcesRoot(tempDir).packageName("com.example");
var t1 = mockTemplateDescription().packageName("com.example").className("JtefooGenerated").name("foo.jte");
var t2 = mockTemplateDescription().packageName("com.example").className("JtebarGenerated").name("bar.jte");

Collection<Path> result = extension.generate(config, Set.of(t1, t2));

assertThat(result).hasSize(1);
assertThat(tempDir.resolve("com/example/package-info.java")).exists();
}

@Test
void multiplePackages() {
var config = mockConfig().generatedSourcesRoot(tempDir).packageName("com.example");
var t1 = mockTemplateDescription().packageName("com.example").className("JtefooGenerated").name("foo.jte");
var t2 = mockTemplateDescription().packageName("com.example.sub").className("JtebarGenerated").name("sub/bar.jte");

Collection<Path> result = extension.generate(config, Set.of(t1, t2));

assertThat(result).hasSize(2);
assertThat(tempDir.resolve("com/example/package-info.java")).exists();
assertThat(tempDir.resolve("com/example/sub/package-info.java")).exists();
}

@Test
void existingPackageInfoIsNotOverwritten() throws IOException {
var config = mockConfig().generatedSourcesRoot(tempDir).packageName("com.example");
var template = mockTemplateDescription().packageName("com.example").className("JtefooGenerated").name("foo.jte");
Path packageDir = tempDir.resolve("com/example");
Files.createDirectories(packageDir);
Path existing = packageDir.resolve("package-info.java");
Files.writeString(existing, "// existing content\n");

extension.generate(config, Set.of(template));

assertThat(Files.readString(existing)).isEqualTo("// existing content\n");
}

@Test
void emptyTemplates() {
var config = mockConfig().generatedSourcesRoot(tempDir).packageName("com.example");

Collection<Path> result = extension.generate(config, Set.of());

assertThat(result).isEmpty();
}

@Test
void nullSourcesRoot() {
var config = mockConfig().packageName("com.example");

Collection<Path> result = extension.generate(config, Set.of(
mockTemplateDescription().packageName("com.example").className("JtefooGenerated").name("foo.jte")));

assertThat(result).isEmpty();
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ nav:
- "Kotlin Templates": kotlin.md
- "Extensions":
- "jte-models Facade Generator": jte-models.md
- "jte-nullmarked NullMarked Annotation Generator": jte-nullmarked.md
- "Extensions API": jte-extension-api.md
- "Spring Boot Support":
- "Spring Boot Starter 4": spring-boot-starter-4.md
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<module>jte-extension-api</module>
<module>jte-extension-api-mocks</module>
<module>jte-native-resources</module>
<module>jte-nullmarked</module>
<module>jte-models</module>
<module>jte-watcher</module>
<module>jte-maven-plugin</module>
Expand All @@ -64,6 +65,7 @@
<module>test/jte-hotreload-test</module>
<module>test/jte-test-report</module>
<module>test/jte-runtime-cp-test-models</module>
<module>test/jte-runtime-cp-test-nullmarked</module>
<module>jte-deploy-nexus</module>
</modules>

Expand Down
29 changes: 29 additions & 0 deletions test/jte-runtime-cp-test-nullmarked-gradle/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
plugins {
id 'java'
id 'gg.jte.gradle' version '3.2.4-SNAPSHOT'
}

repositories {
mavenCentral()
mavenLocal()
}

group = 'gg.jte.testgroup'

test {
useJUnitPlatform()
}

dependencies {
implementation('gg.jte:jte-runtime:3.2.4-SNAPSHOT')
implementation('org.jspecify:jspecify:1.0.0')
testImplementation('org.junit.jupiter:junit-jupiter:5.9.0')
testImplementation('org.assertj:assertj-core:3.27.7')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.9.0')
jteGenerate('gg.jte:jte-nullmarked:3.2.4-SNAPSHOT')
}

jte {
generate()
jteExtension('gg.jte.nullmarked.NullMarkedExtension')
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading