Skip to content

SONARJAVA-6421 Extract @Profile expressions when gathering bean definitions - #6071

Draft
NoemieBenard wants to merge 3 commits into
nb/sonarjava-6889-extend-modelfrom
nb/sonarjava-6421-add-profile-support
Draft

SONARJAVA-6421 Extract @Profile expressions when gathering bean definitions#6071
NoemieBenard wants to merge 3 commits into
nb/sonarjava-6889-extend-modelfrom
nb/sonarjava-6421-add-profile-support

Conversation

@NoemieBenard

@NoemieBenard NoemieBenard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • BeanDefinitionGatherer now extracts @Profile expressions (single or array-valued) from stereotype-annotated classes and @Bean methods, storing them on BeanDefinitionHolder via the existing profiles(...) builder step.
  • For @Bean methods, the method's own @Profile takes precedence over the one declared on the enclosing @Configuration/@Component class; if the method has none, it inherits the class's.
  • Updates the bean-definition cache serialization format to persist the new field (profiles inserted right after isPrimary).

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6421

Comment on lines +470 to +479
@Nullable
private static String composeProfiles(@Nullable String classProfiles, @Nullable String ownProfiles) {
if (classProfiles == null) {
return ownProfiles;
}
if (ownProfiles == null) {
return classProfiles;
}
return classProfiles + PROFILE_AND_SEPARATOR + ownProfiles;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: composeProfiles branch for method-only @Profile is untested

composeProfiles has three branches, but the parameterized profileArguments cases only exercise class-only (inheritedProfileBean → "prod"), both-present (ownProfileBean → "prod;test") and both-absent (simpleComponent → null); no test resource declares a @Profile on a @Bean method inside a class without a class-level @Profile (grep over src/test/files/springcontext shows @Profile only in ProfiledComponent, MultiProfileComponent and ProfiledConfigurationWithBeanMethods, the latter always class-annotated). The classProfiles == null && ownProfiles != null path — the common Spring pattern of an unprofiled @Configuration with profile-gated @Bean methods — is therefore uncovered, so a future regression that drops the method-level expression in that case would not be caught. Add a @Bean-level-only @Profile fixture and a corresponding argument row.

Add a fixture with a method-level-only @Profile and assert the composed value is the method's own expression.:

// src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java
@Configuration
class ConfigurationWithProfiledBeanMethod {
  @Profile("test")
  @Bean
  ApplicationContext methodOnlyProfileBean() { return null; }
}

// BeanDefinitionGathererTest#profileArguments
// @Bean method's own @Profile is kept when the enclosing class has none
Arguments.of("src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java", "methodOnlyProfileBean", "test")
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 3 findings

Extracts @Profile expressions from stereotype-annotated classes and @Bean methods, with method-level @Profile taking precedence over class-level when both are present. Updates cache serialization to persist the new profiles field. Consider adding test coverage for the method-only @Profile branch—the case where a @Bean method has @Profile but its enclosing class does not—to catch potential regressions.

💡 Quality: composeProfiles branch for method-only @Profile is untested

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:470-479 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:160-170 📄 java-frontend/src/test/files/springcontext/ProfiledConfigurationWithBeanMethods.java:8-21

composeProfiles has three branches, but the parameterized profileArguments cases only exercise class-only (inheritedProfileBean → "prod"), both-present (ownProfileBean → "prod;test") and both-absent (simpleComponent → null); no test resource declares a @Profile on a @Bean method inside a class without a class-level @Profile (grep over src/test/files/springcontext shows @Profile only in ProfiledComponent, MultiProfileComponent and ProfiledConfigurationWithBeanMethods, the latter always class-annotated). The classProfiles == null && ownProfiles != null path — the common Spring pattern of an unprofiled @Configuration with profile-gated @Bean methods — is therefore uncovered, so a future regression that drops the method-level expression in that case would not be caught. Add a @Bean-level-only @Profile fixture and a corresponding argument row.

Add a fixture with a method-level-only @Profile and assert the composed value is the method's own expression.
// src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java
@Configuration
class ConfigurationWithProfiledBeanMethod {
  @Profile("test")
  @Bean
  ApplicationContext methodOnlyProfileBean() { return null; }
}

// BeanDefinitionGathererTest#profileArguments
// @Bean method's own @Profile is kept when the enclosing class has none
Arguments.of("src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java", "methodOnlyProfileBean", "test")
✅ 2 resolved
Quality: Profiles cache round-trip is untested (non-empty field never exercised)

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:196-198 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:282-284 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:372 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:492 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:566 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:755
Every cache test in the suite was updated with an empty profiles field (|false|||...), and the parse-based profile_annotation_is_captured tests never touch the cache, so the new non-null branches — Base64 encoding at lines 196-198 and Base64 decoding at lines 282-284 — are never executed by any test. A regression in the encode/decode pair (e.g. a field-order mistake) would ship green. Add one round-trip case with a real profile value.

Bug: Class-level @Profile is discarded when @bean method has its own

📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:64-65 📄 java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:365-366 📄 java-frontend/src/test/files/springcontext/ProfiledConfigurationWithBeanMethods.java:8-21 📄 java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:166-168
In Spring, a class-level @Profile and a method-level @Profile are AND-ed: ConfigurationClassParser skips the whole @Configuration class when the class condition does not match, so the @Bean method's condition is additive, never an override. With the new fixture ProfiledConfigurationWithBeanMethods, ownProfileBean is stored with profiles = "test" (line 366 picks ownProfiles and drops classProfiles), but the bean is actually only registered when both prod and test are active — so a consumer of BeanDefinitionHolder.getProfiles() will consider it a live candidate under a test-only profile set, which it never is. Compose the two conditions instead of overriding (and update the javadoc at lines 64-65 accordingly).

🤖 Prompt for agents
Code Review: Extracts `@Profile` expressions from stereotype-annotated classes and `@Bean` methods, with method-level `@Profile` taking precedence over class-level when both are present. Updates cache serialization to persist the new `profiles` field. Consider adding test coverage for the method-only `@Profile` branch—the case where a `@Bean` method has `@Profile` but its enclosing class does not—to catch potential regressions.

1. 💡 Quality: composeProfiles branch for method-only @Profile is untested
   Files: java-frontend/src/main/java/org/sonar/java/model/springcontext/BeanDefinitionGatherer.java:470-479, java-frontend/src/test/java/org/sonar/java/model/springcontext/BeanDefinitionGathererTest.java:160-170, java-frontend/src/test/files/springcontext/ProfiledConfigurationWithBeanMethods.java:8-21

   `composeProfiles` has three branches, but the parameterized `profileArguments` cases only exercise class-only (`inheritedProfileBean` → "prod"), both-present (`ownProfileBean` → "prod;test") and both-absent (`simpleComponent` → null); no test resource declares a `@Profile` on a `@Bean` method inside a class without a class-level `@Profile` (grep over `src/test/files/springcontext` shows `@Profile` only in ProfiledComponent, MultiProfileComponent and ProfiledConfigurationWithBeanMethods, the latter always class-annotated). The `classProfiles == null && ownProfiles != null` path — the common Spring pattern of an unprofiled `@Configuration` with profile-gated `@Bean` methods — is therefore uncovered, so a future regression that drops the method-level expression in that case would not be caught. Add a `@Bean`-level-only `@Profile` fixture and a corresponding argument row.

   Fix (Add a fixture with a method-level-only @Profile and assert the composed value is the method's own expression.):
   // src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java
   @Configuration
   class ConfigurationWithProfiledBeanMethod {
     @Profile("test")
     @Bean
     ApplicationContext methodOnlyProfileBean() { return null; }
   }
   
   // BeanDefinitionGathererTest#profileArguments
   // @Bean method's own @Profile is kept when the enclosing class has none
   Arguments.of("src/test/files/springcontext/ConfigurationWithProfiledBeanMethod.java", "methodOnlyProfileBean", "test")

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqube-next

sonarqube-next Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Quality Gate failed Quality Gate failed

Failed conditions
Vulnerability dependency risks too severe (required < 'medium' severity)

See analysis details on SonarQube

Integer.parseInt(spanParts[2]),
Integer.parseInt(spanParts[3]));
boolean isPrimary = Boolean.parseBoolean(fields[4]);
String profiles = !fields[5].isEmpty()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This serialization-deserialization becomes more and more complex. I'd say we should think about using some format, JSON or maybe something binary like Protobuf. Let's create JIRA ticket as follow-up for this in epic https://sonarsource.atlassian.net/browse/SONARJAVA-6237.

.orElse(null);
}

@Nullable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The same as for PR #6068. After extracting this method please add comment.

@asya-vorobeva asya-vorobeva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Profile annotation luckily does not evaluate SpEL expressions. But it supports syntax like this:
@Profile("dev & !test")

More precisely, it supports !, &, | operators.
Would be great to add support for such evaluation. But I'd suggest to do it in separate PR.

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