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

Further Support Null Analysis #47

Merged
merged 42 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6eac7c8
allow empty records
SentryMan Jan 7, 2024
ed4d471
support unnamed package records
SentryMan Jan 7, 2024
d77701b
Merge branch 'main' into empty
SentryMan Jan 7, 2024
b713d7f
defaults
SentryMan Jan 8, 2024
4521d86
Update README.md
SentryMan Jan 8, 2024
b380c83
Update README.md
SentryMan Jan 8, 2024
d9c9166
Update README.md
SentryMan Jan 8, 2024
f78e719
start
SentryMan Jan 8, 2024
cd7d4ad
done
SentryMan Jan 8, 2024
16b33b4
Merge branch 'main' into nullability
SentryMan Jan 8, 2024
fd46274
Update module-info.java
SentryMan Jan 8, 2024
7c2bd5f
Merge branch 'nullability' of https://github.com/SentryMan/avaje-reco…
SentryMan Jan 8, 2024
ae09ed3
Update RecordBuilder.java
SentryMan Jan 8, 2024
a6a316f
try checker framework
SentryMan Jan 8, 2024
fd82e31
Revert "try checker framework"
SentryMan Jan 8, 2024
9ce8fb8
get checker to work
SentryMan Jan 8, 2024
50956e6
make some progess
SentryMan Jan 8, 2024
d547e45
finally get it working
SentryMan Jan 8, 2024
990a565
null away and error prone
SentryMan Jan 8, 2024
70d71fd
Update pom.xml
SentryMan Jan 8, 2024
491d116
nullaway
SentryMan Jan 8, 2024
4a05add
Update README.md
SentryMan Jan 8, 2024
8f430a1
format
SentryMan Jan 8, 2024
de4d8bf
helpful error message
SentryMan Jan 9, 2024
eb1a17e
Merge remote-tracking branch 'upstream/main' into nullability
SentryMan Jan 9, 2024
3053401
Update ImportPrism.java
SentryMan Jan 9, 2024
202c2d9
support partial compile
SentryMan Jan 9, 2024
a738aa2
Merge branch 'main' into nullability
SentryMan Jan 13, 2024
cf89a7e
support @NullMarked
SentryMan Jan 16, 2024
76ac62e
support nullUnmarked
SentryMan Jan 16, 2024
a314998
Merge branch 'main' into nullability
SentryMan Jan 29, 2024
3d9d497
Merge branch 'main' into nullability
SentryMan Feb 22, 2024
2652118
Merge branch 'main' into nullability
SentryMan Jun 15, 2024
041f59b
Merge branch 'main' into nullability
SentryMan Jul 15, 2024
c463949
Merge branch 'main' into nullability
rbygrave Aug 30, 2024
0aec148
follow pr suggestions
SentryMan Aug 30, 2024
f5fa4ad
Merge branch 'nullability' of https://github.com/SentryMan/avaje-reco…
SentryMan Aug 30, 2024
0912aed
disable EA
SentryMan Aug 30, 2024
d447400
Update ImportPrism.java
SentryMan Aug 30, 2024
6ad8afd
Update pom.xml
SentryMan Aug 30, 2024
5e06fd2
fix nonnull detection
SentryMan Aug 30, 2024
5737afc
Update Utils.java
SentryMan Aug 30, 2024
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
10 changes: 10 additions & 0 deletions .mvn/jvm.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ Uses Annotation processing to generate builders for records.
## Distinguishing features
- By default, Collection/Optional Types will not be null (an empty collection/optional will be provided)
- We can choose the default value of a record component in the generated builder
- Copies nullability annotations to the generated setters to aid in static analysis

- Support for generating Checker/NullAway compliant builders for static null analysis.
## Usage
### 1. Add dependency:
```xml
Expand All @@ -22,7 +21,7 @@ Uses Annotation processing to generate builders for records.
</dependency>
```

When working with Java modules you need to add the annotation module as a static dependency.
Add the annotation module as a static dependency when working with Java modules.
```java
module my.module {
requires static io.avaje.recordbuilder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.avaje.recordbuilder.internal;

import java.util.List;

import javax.lang.model.type.TypeMirror;

public interface BuilderPrism {

default boolean imported() {
return this instanceof ImportPrism;
}

/**
* Returns a Boolean representing the value of the {@code boolean public abstract boolean
* getters() } member of the Annotation.
*
* @see io.avaje.recordbuilder.RecordBuilder.Import#getters()
*/
Boolean getters();

/**
* Returns a Boolean representing the value of the {@code boolean public abstract boolean
* enforceNullSafety() } member of the Annotation.
*
* @see io.avaje.recordbuilder.RecordBuilder.Import#enforceNullSafety()
*/
Boolean enforceNullSafety();

/**
* Returns a TypeMirror representing the value of the {@code java.lang.Class<? extends
* java.lang.annotation.Annotation> public abstract Class<? extends
* java.lang.annotation.Annotation> nullableAnnotation() } member of the Annotation.
*
* @see io.avaje.recordbuilder.RecordBuilder.Import#nullableAnnotation()
*/
TypeMirror nullableAnnotation();

/**
* Returns a List&lt;TypeMirror&gt; representing the value of the {@code public abstract
* Class<?>[] builderInterfaces() } member of the Annotation.
*
* @see io.avaje.recordbuilder.RecordBuilder.Import#builderInterfaces()
*/
List<TypeMirror> builderInterfaces();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@

import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

public class ClassBodyBuilder {

private ClassBodyBuilder() {}

static String createClassStart(
TypeElement type, String typeParams, boolean isImported, String packageName) {
BuilderPrism prism,
TypeElement type,
String typeParams,
boolean isImported,
String packageName) {

final var components = type.getRecordComponents();

final var shortName = type.getSimpleName().toString();
if (type.getEnclosingElement() instanceof TypeElement) {
isImported = true;
Expand All @@ -39,6 +43,14 @@ static String createClassStart(

final RecordModel rm = new RecordModel(type, isImported, components, utype);
rm.initialImports();
rm.nullableAnnotation(GlobalSettings.nullableAnnotation().orElse(prism.nullableAnnotation().toString()));
var implementsStr =
prism.builderInterfaces().stream()
.map(TypeMirror::toString)
.peek(rm::addImport)
.map(ProcessorUtils::shortType)
.collect(joining(", "))
.transform(s -> s.isEmpty() ? s : "implements " + s);
final String fieldString = rm.fields();
final var imports = rm.importsFormat();
final var numberOfComponents = components.size();
Expand All @@ -48,11 +60,12 @@ static String createClassStart(
final String builderFrom =
builderFrom(components).transform(s -> numberOfComponents > 5 ? "\n " + s : s);
final String build =
build(components).transform(s -> numberOfComponents > 6 ? "\n " + s : s);
build(components, prism).transform(s -> numberOfComponents > 6 ? "\n " + s : s);
return classTemplate(
packageName,
imports,
shortName,
implementsStr,
fieldString,
constructorParams,
constructorBody,
Expand Down Expand Up @@ -85,7 +98,18 @@ private static String builderFrom(List<? extends RecordComponentElement> compone
.collect(joining(", "));
}

private static String build(List<? extends RecordComponentElement> components) {
return components.stream().map(RecordComponentElement::getSimpleName).collect(joining(", "));
private static String build(
List<? extends RecordComponentElement> components, BuilderPrism prism) {

return components.stream()
.map(
element -> {
final var simpleName = element.getSimpleName();
return !Utils.isNullableType(UType.parse(element.asType()).mainType())
&& Utils.isNonNullable(element, prism)
? "requireNonNull(%s, \"%s\")".formatted(simpleName, simpleName)
: simpleName;
})
.collect(joining(", "));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package io.avaje.recordbuilder.internal;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import javax.annotation.processing.Generated;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.type.TypeMirror;
import java.util.HashMap;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;

/** A Prism representing a {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} annotation. */
@Generated("avaje-prism-generator")
final class GlobalConfigPrism {
/** store prism value of getters */
private final Boolean _getters;

/** store prism value of enforceNullSafety */
private final Boolean _enforceNullSafety;

/** store prism value of nullableAnnotation */
private final TypeMirror _nullableAnnotation;

public static final String PRISM_TYPE = "io.avaje.recordbuilder.RecordBuilder.GlobalConfig";

/**
* An instance of the Values inner class whose
* methods return the AnnotationValues used to build this prism.
* Primarily intended to support using Messager.
*/
final Values values;

/** Returns true if the mirror is an instance of {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} is present on the element, else false.
*
* @param mirror mirror.
* @return true if prism is present.
*/
static boolean isInstance(AnnotationMirror mirror) {
return getInstance(mirror) != null;
}

/** Returns true if {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} is present on the element, else false.
*
* @param element element.
* @return true if annotation is present on the element.
*/
static boolean isPresent(Element element) {
return getInstanceOn(element) != null;
}

/** Return a prism representing the {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} annotation present on the given element.
* similar to {@code element.getAnnotation(GlobalConfig.class)} except that
* an instance of this class rather than an instance of {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig}
* is returned.
*
* @param element element.
* @return prism on element or null if no annotation is found.
*/
static GlobalConfigPrism getInstanceOn(Element element) {
final var mirror = getMirror(element);
if (mirror == null) return null;
return getInstance(mirror);
}

/** Return a Optional representing a nullable {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} annotation on the given element.
* similar to {@code element.getAnnotation(io.avaje.recordbuilder.RecordBuilder.GlobalConfig.class)} except that
* an Optional of this class rather than an instance of {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig}
* is returned.
*
* @param element element.
* @return prism optional for element.
*/
static Optional<GlobalConfigPrism> getOptionalOn(Element element) {
final var mirror = getMirror(element);
if (mirror == null) return Optional.empty();
return getOptional(mirror);
}

/** Return a prism of the {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig} annotation from an annotation mirror.
*
* @param mirror mirror.
* @return prism for mirror or null if mirror is an incorrect type.
*/
static GlobalConfigPrism getInstance(AnnotationMirror mirror) {
if (mirror == null || !PRISM_TYPE.equals(mirror.getAnnotationType().toString())) return null;

return new GlobalConfigPrism(mirror);
}

/** Return an Optional representing a nullable {@link GlobalConfigPrism @GlobalConfigPrism} from an annotation mirror.
* similar to {@code e.getAnnotation(io.avaje.recordbuilder.RecordBuilder.GlobalConfig.class)} except that
* an Optional of this class rather than an instance of {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig}
* is returned.
*
* @param mirror mirror.
* @return prism optional for mirror.
*/
static Optional<GlobalConfigPrism> getOptional(AnnotationMirror mirror) {
if (mirror == null || !PRISM_TYPE.equals(mirror.getAnnotationType().toString())) return Optional.empty();

return Optional.of(new GlobalConfigPrism(mirror));
}

private GlobalConfigPrism(AnnotationMirror mirror) {
for (final ExecutableElement key : mirror.getElementValues().keySet()) {
memberValues.put(key.getSimpleName().toString(), mirror.getElementValues().get(key));
}
for (final ExecutableElement member : ElementFilter.methodsIn(mirror.getAnnotationType().asElement().getEnclosedElements())) {
defaults.put(member.getSimpleName().toString(), member.getDefaultValue());
}
_getters = getValue("getters", Boolean.class);
_enforceNullSafety = getValue("enforceNullSafety", Boolean.class);
_nullableAnnotation = getValue("nullableAnnotation", TypeMirror.class);
this.values = new Values(memberValues);
this.mirror = mirror;
this.isValid = valid;
}

/**
* Returns a Boolean representing the value of the {@code boolean public abstract boolean getters() } member of the Annotation.
* @see io.avaje.recordbuilder.RecordBuilder.GlobalConfig#getters()
*/
public Boolean getters() { return _getters; }

/**
* Returns a Boolean representing the value of the {@code boolean public abstract boolean enforceNullSafety() } member of the Annotation.
* @see io.avaje.recordbuilder.RecordBuilder.GlobalConfig#enforceNullSafety()
*/
public Boolean enforceNullSafety() { return _enforceNullSafety; }

/**
* Returns a TypeMirror representing the value of the {@code java.lang.Class<? extends java.lang.annotation.Annotation> public abstract Class<? extends java.lang.annotation.Annotation> nullableAnnotation() } member of the Annotation.
* @see io.avaje.recordbuilder.RecordBuilder.GlobalConfig#nullableAnnotation()
*/
public TypeMirror nullableAnnotation() { return _nullableAnnotation; }

/**
* Determine whether the underlying AnnotationMirror has no errors.
* True if the underlying AnnotationMirror has no errors.
* When true is returned, none of the methods will return null.
* When false is returned, a least one member will either return null, or another
* prism that is not valid.
*/
final boolean isValid;

/**
* The underlying AnnotationMirror of the annotation
* represented by this Prism.
* Primarily intended to support using Messager.
*/
final AnnotationMirror mirror;
/**
* A class whose members corespond to those of {@link io.avaje.recordbuilder.RecordBuilder.GlobalConfig @GlobalConfig}
* but which each return the AnnotationValue corresponding to
* that member in the model of the annotations. Returns null for
* defaulted members. Used for Messager, so default values are not useful.
*/
static final class Values {
private final Map<String, AnnotationValue> values;

private Values(Map<String, AnnotationValue> values) {
this.values = values;
}
/** Return the AnnotationValue corresponding to the getters()
* member of the annotation, or null when the default value is implied.
*/
AnnotationValue getters(){ return values.get("getters");}
/** Return the AnnotationValue corresponding to the enforceNullSafety()
* member of the annotation, or null when the default value is implied.
*/
AnnotationValue enforceNullSafety(){ return values.get("enforceNullSafety");}
/** Return the AnnotationValue corresponding to the nullableAnnotation()
* member of the annotation, or null when the default value is implied.
*/
AnnotationValue nullableAnnotation(){ return values.get("nullableAnnotation");}
}

private final Map<String, AnnotationValue> defaults = new HashMap<String, AnnotationValue>(10);
private final Map<String, AnnotationValue> memberValues = new HashMap<String, AnnotationValue>(10);
private boolean valid = true;

private <T> T getValue(String name, Class<T> clazz) {
final T result = GlobalConfigPrism.getValue(memberValues, defaults, name, clazz);
if (result == null) valid = false;
return result;
}

private static AnnotationMirror getMirror(Element target) {
for (final var m : target.getAnnotationMirrors()) {
final CharSequence mfqn = ((TypeElement) m.getAnnotationType().asElement()).getQualifiedName();
if (PRISM_TYPE.contentEquals(mfqn)) return m;
}
return null;
}

private static <T> T getValue(Map<String, AnnotationValue> memberValues, Map<String, AnnotationValue> defaults, String name, Class<T> clazz) {
AnnotationValue av = memberValues.get(name);
if (av == null) av = defaults.get(name);
if (av == null) {
return null;
}
if (clazz.isInstance(av.getValue())) return clazz.cast(av.getValue());
return null;
}

}
Loading
Loading