Skip to content

Add annotation @BehaviorChangeSince for behavior-change flags#1170

Closed
eric-maynard wants to merge 14 commits intoapache:mainfrom
eric-maynard:since-annotation
Closed

Add annotation @BehaviorChangeSince for behavior-change flags#1170
eric-maynard wants to merge 14 commits intoapache:mainfrom
eric-maynard:since-annotation

Conversation

@eric-maynard
Copy link
Contributor

#1124 introduces "behavior change" flags, which are intended to guard contentious or risky behavior changes and to be removed after a short time. This PR codifies that intention by adding a new spotless rule checkStaleBehaviorChangeFlags that will fail during format when a behavior change flag is detected to have been around for more than 2 minor release or for more than 1 major release.

import java.lang.annotation.Target;

/**
* An annotation that should be applied to all BehaviorChangeConfiguration instances. The value here
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add validation for that to the spotless check too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely. I looked into this a bit, but it seems like the "best" way would be via reflection (enumerating all instances of the BehaviorChangeConfiguration type) which feels hacky. At the very least, let me add a check so that no configs are created outside of the correct file.

Copy link
Contributor

Choose a reason for hiding this comment

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

That can be done in a compile-time annotation processor (I think it has access to all elements, even if not annotated).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome, I'll look into that and update the PR. For now, I added a lint check to try and make sure the flags are contained within the one file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After some research, I have one doubt about the above approach. If we want to scan for BehaviorChangeConfiguration instances without the annotation, is an annotation processor really the right choice? I am wondering if this, too, needs to be done in the linter.

Copy link
Contributor

Choose a reason for hiding this comment

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

Cross references are inevitable. For example, immutables generate code from annotations that is referenced by some other hand-written classes. @Overrides annotations relate to base classes, which do not have the annotation itself. I think it is quite normal for annotation processors to "look around" in the codebase.

By the way, the immutables lib can be a source of inspiration here, I guess 😉

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 tried adding a processor BehaviorChangeProcessor and registering it, but I don't think it actually runs based on my testing. If you're able to take a look and you see a problem, please do let me know

super(key, description, defaultValue, catalogConfig);
}

@BehaviorChangeConfigurationSince("1.0.0")
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to fix the versions.txt to point to the actual version of the code, I think. These values should be set to whatever is there. Currently, that's 1.0.0

*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BehaviorChangeConfigurationSince {
Copy link
Contributor

Choose a reason for hiding this comment

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

Side note: it would be great to consider this annotation in tools/config-docs/generator

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 mean to move it there?

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I mean auto-generated config docs should ideally consider this annotation

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 see, I've looked through the generator package but I'm not sure I fully understand what it would mean for the docs to consider the annotation. I guess the idea would be to have the docs about each BehaviorChangeConfiguration describe what version the configuration was created in, etc.? In fact, I don't know if we even want to document the BehaviorChangeConfigurations

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe, in an open source project all config that a user may want to change should be documented... but we can leave BehaviorChangeConfigurations for later, of course.


override fun apply(input: String): String {
val versionRegex =
"""@BehaviorChangeScope\s*\(\s*since\s*=\s*"(\d+\.\d+\.\d+)"(?:\s*,\s*expires\s*=\s*"(\d+\.\d+\.\d+)")?\s*\)"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Will it work if the annotation is split across multiple lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It more or less should work because of all the \s* -- but agreed that a lint check is relatively brittle here. We can always add another check later though

val (majorA, minorA, patchA) = annotatedVersion.split(".").map { it.toInt() }
if (expiresVersion == null) {
val (majorB, minorB, _) = polarisVersion.split(".").map { it.toInt() }
return (majorB > majorA || (majorB == majorA && minorB > minorA + 1))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to have only on place for comparing versions, but derive the "expires" version from the "added" version automatically?

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 updated the PR to try this, but the logic might be less intuitive at first glance. Let me know what you think!

Diagnostic.Kind.NOTE,
"Field "
+ field.getSimpleName()
+ " is correctly annotated with @BehaviorChange",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it worth logging?

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 it's not, but I noticed the processor isn't working so I added this to debug -- see my comment here. If you have any tips on fixing this, that would be awesome. I think the annotation processor is a good suggestion

public class BehaviorChangeProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to move version/expiry checks here? It think it would be nicer from the overall design POV.

Build scripts could probably inject "current version" into system properties. WDYT?

Copy link
Contributor Author

@eric-maynard eric-maynard Mar 19, 2025

Choose a reason for hiding this comment

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

I really prefer to do things like this in a linter. We can always do it in both, too.

Copy link
Contributor

Choose a reason for hiding this comment

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

having both looks awkward to me... I'd prefer to move the logic and keep only one of them (up to you which one).

@dimas-b
Copy link
Contributor

dimas-b commented Mar 31, 2025

@eric-maynard : how about converting built-time checks to unit tests? I believe tests can scan pre-compiled classes for annotations and verity that all required constants are annotated. Also we can probably inject the current version number from gradle and validate expiry... as for me it looks simpler and easier to debug that build-time checks... WDYT?

@eric-maynard
Copy link
Contributor Author

Good idea @dimas-b, I will try to do just that. Putting the PR in draft for now

@eric-maynard eric-maynard marked this pull request as draft April 1, 2025 16:52
@github-actions
Copy link

github-actions bot commented May 2, 2025

This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@github-actions github-actions bot added the stale label May 2, 2025
@github-project-automation github-project-automation bot moved this from PRs In Progress to Done in Basic Kanban Board May 21, 2025
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 this pull request may close these issues.

2 participants