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

Generator Should Read ExternalImportType Safety #1944

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from

Conversation

lsingh123
Copy link
Contributor

@lsingh123 lsingh123 commented Feb 1, 2023

If an Alias, Field, or Argument is an external import type, use the underlying import's safety instead of the declared safety (which will be empty). Converts all usages of AliasDefinition.getSafety(), FieldDefinition.getSafety(), and ArgumentDefinition.getSafety() to the corresponding wrapper methods in SafetyUtils.java.

@lsingh123 lsingh123 marked this pull request as draft February 2, 2023 16:50
@lsingh123 lsingh123 closed this Feb 2, 2023
@lsingh123
Copy link
Contributor Author

#1945 is a better solution

@lsingh123 lsingh123 reopened this Feb 8, 2023
@lsingh123 lsingh123 changed the title WIP Generator Should Read ExternalImportType Safety Feb 8, 2023
@lsingh123 lsingh123 marked this pull request as ready for review February 9, 2023 19:15
}

@JsonSetter(value = "optionalSafeExternalLong", nulls = Nulls.SKIP)
public Builder optionalSafeExternalLong(@Nonnull Optional<? extends Long> optionalSafeExternalLong) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, not sure if we want the parameter to have a safety annotation, or if we don't support wrapping external references in optional

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i am actually going to handle optionals wrapping externals, pushing that up soon

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I think we'd end up with Optional<@Safe ? extends Long>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want @Safe Optional<@Safe ? extends Long> like here? Or would we omit the external annotation for external imports?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the linked example should ideally only have a single @Safe:

public Builder item(@Nonnull Optional<@Safe String> item) {

Copy link
Contributor Author

@lsingh123 lsingh123 Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity:

Poet has a bug that will make handling containers of external imports hard (like optional). We use WildcardTypeName to represent the wildcard type extending an external import. But when emitting, WildcardTypeName essentially ignores its annotations, which I verified in this (extremely rough) test. So in order to generate code that looks like @Safe ? extends Long> we would have to do something extremely hacky, like eschew javapoet and just stick in a string which doesn't feel......great.

So we decided to settle for the annotation on the wrapper itself, as in @Safe Optional<...>

@lsingh123 lsingh123 force-pushed the lsingh/external-imports-log-safety branch from 2c16819 to e29e2d8 Compare February 9, 2023 20:33
FieldSpec field = enriched.poetSpec();
return BeanBuilderAuxiliarySettersUtils.createItemSetterBuilder(enriched, itemType, typeMapper, builderClass)
return BeanBuilderAuxiliarySettersUtils.createItemSetterBuilder(
Copy link
Contributor Author

@lsingh123 lsingh123 Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previously, item setters for collection types didn't have safety annotations, even for primitive collections. For example, a field of type List<boolean> would generate the following setter:

       public Builder listBoolean(String listBoolean) {
            checkNotBuilt();
            this.listBoolean.add(listBoolean);
            return this;
        }

Now, both primitive and external import collections generate item setters that look like:

        public Builder listBoolean(@Safe String listBoolean) {
            checkNotBuilt();
            this.listBoolean.add(listBoolean);
            return this;
        }


Class<?> builderSubclass = getMatchingSubclass(SafeExternalLongExample.class, "$Builder");
assertFieldTypeParamHasAnnotation(builderSubclass, "safeExternalLongSet", "Long", Safe.class);
assertMethodParamHasAnnotation(builderSubclass, "safeExternalLongSet", "safeExternalLongSet", Safe.class);
Copy link
Contributor Author

@lsingh123 lsingh123 Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this includes the item setter and the collection setter because the internal method uses allMatch

@lsingh123 lsingh123 force-pushed the lsingh/external-imports-log-safety branch from be32776 to 3c5b062 Compare February 10, 2023 21:45
FieldSpec field = enriched.poetSpec();
return publicSetter(enriched, returnClass).addParameter(typeMapper.getClassName(itemType), field.name);
return publicSetter(enriched, returnClass)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@changelog-app
Copy link

changelog-app bot commented Feb 13, 2023

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Adds support for log safety annotations to external import types. For example, an alias of an external reference type will now have safety annotations on the class, getters, and setters.

Check the box to generate changelog(s)

  • Generate changelog entry

@lsingh123 lsingh123 force-pushed the lsingh/external-imports-log-safety branch from 92673fe to da8b782 Compare February 13, 2023 19:12
@@ -113,7 +115,7 @@ public Optional<LogSafety> visitUnion(UnionDefinition value) {
return with(value.getTypeName(), () -> {
Optional<LogSafety> safety = UNKNOWN_UNION_VARINT_SAFETY;
for (FieldDefinition variant : value.getUnion()) {
safety = combine(safety, getSafety(variant.getType(), variant.getSafety()));
safety = combine(safety, getSafety(variant.getType(), SafetyUtils.getMaybeExternalSafety(variant)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, I don't think any of the SafetyUtils utility-method invocations used in this class are necessary. This class will walk the tree until it finds something that declares safety, so the updated visitExternal(ExternalReference value) should take care of it.

Comment on lines +37 to +56
public static Optional<LogSafety> getMaybeExternalSafety(AliasDefinition alias) {
if (alias.getAlias().accept(WrapsExternalType.INSTANCE)) {
return alias.getAlias().accept(GetMaybeExternalSafety.INSTANCE);
}
return alias.getSafety();
}

public static Optional<LogSafety> getMaybeExternalSafety(FieldDefinition field) {
if (field.getType().accept(WrapsExternalType.INSTANCE)) {
return field.getType().accept(GetMaybeExternalSafety.INSTANCE);
}
return field.getSafety();
}

public static Optional<LogSafety> getMaybeExternalSafety(ArgumentDefinition argument) {
if (argument.getType().accept(WrapsExternalType.INSTANCE)) {
return argument.getType().accept(GetMaybeExternalSafety.INSTANCE);
}
return argument.getSafety();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might reframe and refactor these a bit to decouple behavior from external types. There's a class of values (applies to fields, arguments, and aliases) which require their safety to be explicitly listed. So, given a Type, we need to determine whether the generated class declares safety itself, or if we need to add an annotation where the type is used.

Primitives (with the exception of bearer-token) and external-type-imports require safety at each place they're used. Determining the safety value can be handled using the existing SafetyEvaluator class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so I could instead think of this class as getting the usage time safety, which, for primitives, is declared in the wrapper type (field, argument, alias) itself and for external imports, is declared in the external-import-type itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

Comment on lines +19 to +20
externalLong:
type: DangerousLong
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a few more args:

  • declared-safe external imported long
  • external imported long without any safety information declared
  • Alias of each of the existing examples

Comment on lines +42 to +50
@Test
public void testAliasAnnotations() {
assertThat(SafeExternalLongAlias.class).hasAnnotation(Safe.class);

assertMethodHasAnnotation(SafeExternalLongAlias.class, "toString", Safe.class);
assertMethodHasAnnotation(SafeExternalLongAlias.class, "get", Safe.class);
assertMethodParamHasAnnotation(SafeExternalLongAlias.class, "valueOf", "value", Safe.class);
assertMethodParamHasAnnotation(SafeExternalLongAlias.class, "of", "value", Safe.class);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could go either way on reflection-based testing. We have tests which generate integrationInput, so future changes will have clear diffs which can be reviewed more clearly than reading reflection code, but that does rely on the reviewer to understand and account for all changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think there's any harm in having both? I see your point about the integrationInput being more readable, but I feel like external import safety is kind of brittle, because if someone forgets to use the SafetyUtils wrappers then we lose safety markings. So I thought a test at least provides some in-your-face protection against future breaks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally fine to have both :-)

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

Successfully merging this pull request may close these issues.

2 participants