diff --git a/README.md b/README.md
index ad77b8a..326104f 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,13 @@ Additionally, some Error Prone plugins are included:
## Usage
-Enable a quality profile including some rules, for NullAway you will need to configure the list of annotated packages
+Enable a quality profile including some rules from the Error Prone, NullAway, SLF4J, or Picnic repositories.
## Compatibility
-The plugin is compatible with SonarQube from version 9.9 LTS through 10.x and 25.x
+The plugin is compatible with SonarQube 9.9 LTS, 10.x, 25.x, and 26.x.
-The Sonar analyzer and Error Prone must run on JDK 11 or newer but can analyze Java 8 code.
-When running on JDK 16 or newer add the following options due to [JEP 396](https://openjdk.java.net/jeps/396):
+The analysis must run on JDK 17 or newer. Due to [JEP 396](https://openjdk.java.net/jeps/396), the following JVM options are required for the scanner process:
```
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
@@ -32,24 +31,43 @@ When running on JDK 16 or newer add the following options due to [JEP 396](https
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
```
-See [.mvn/jvm.config](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/.mvn/jvm.config) for a way to do it with Maven and [gradle.properties](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/gradle.properties) for a way to do it with Gradle
+See [.mvn/jvm.config](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/.mvn/jvm.config) for Maven and [gradle.properties](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/gradle.properties) for Gradle examples.
-From SonarQybe 10.6 the scanner also auto provisions a JRE and runs the analysis off that JVM. Since the JRE does not include the required compiler module, this needs to be disabled with `sonar.scanner.skipJreProvisioning=true`.
+From SonarQube 10.6, the scanner auto-provisions a JRE without the `jdk.compiler` module. Disable this with `sonar.scanner.skipJreProvisioning=true`.
-When these options are not set you will receive errors:
+Without these options you will see:
```
-Exception in thread "main" java.util.ServiceConfigurationError: com.google.errorprone.bugpatterns.BugChecker: Provider ... could not be instantiated
-...
-Caused by: java.lang.IllegalAccessError: class ... (in unnamed module @...) cannot access class com.sun.tools.javac.code.Symbol (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.code to unnamed module @...
+java.lang.IllegalAccessError: ... cannot access class com.sun.tools.javac.code.Symbol (in module jdk.compiler)
```
-## NullAway configuration
-
-NullAway needs to be configured with the `nullaway.annotated.packages` option, for instance:
-
-```
-nullaway.annotated.packages=com.foo,org.bar
-```
+## Configuration
+
+Options can be set in two places:
+1. **SonarQube Server** — under Project Settings, use the keys listed below.
+2. **Build tool** (Gradle/Maven) — prefix the key with `sonar.` (e.g. `sonar.nullaway.annotated.packages`). The scanner only forwards `sonar.*` properties from build tools.
+
+### NullAway options
+
+| Key | Type | Description |
+|-----|------|-------------|
+| `nullaway.annotated.packages` | multi-value | Packages to check for null safety. Required unless `nullaway.only.null.marked` is set. |
+| `nullaway.unannotated.packages` | multi-value | Sub-packages to exclude from the annotated package list. |
+| `nullaway.unannotated.classes` | multi-value | Classes within annotated packages to treat as unannotated. |
+| `nullaway.known.initializers` | multi-value | Fully qualified method names that NullAway should treat as initializers. |
+| `nullaway.field.annotations` | multi-value | Annotations that exclude fields from initialization checks. |
+| `nullaway.only.null.marked` | boolean | Only check code within `@NullMarked` scope. Package-scope options are ignored when enabled. |
+
+### ErrorAway options
+
+| Key | Type | Description |
+|-----|------|-------------|
+| `erroraway.classpath.maven.coordinates` | multi-value | Maven coordinates of dependencies needed to compile the project. |
+| `erroraway.annotation.processors.maven.coordinates` | multi-value | Maven coordinates of annotation processors. |
+| `erroraway.maven.repositories` | multi-value | Maven remote repository URLs. |
+| `erroraway.maven.local.repository` | string | Path to the Maven local repository. |
+| `erroraway.maven.user.settings.file` | string | Path to the Maven user settings file. |
+| `erroraway.maven.work.offline` | boolean | Let Maven work offline. |
+| `erroraway.maven.use.temp.local.repository` | boolean | Use a temporary folder for the Maven local repository. |
## Developing the ErrorAway plugin
diff --git a/pom.xml b/pom.xml
index fb9df45..5ad2535 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,7 @@
5.6.0.2578
2.42.0
- 0.12.7
+ 0.12.15
0.1.29
0.25.0
diff --git a/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java b/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java
index cd372f7..f14065c 100644
--- a/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java
+++ b/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java
@@ -35,7 +35,7 @@ public final class ErrorAwayRulesMapping {
public static final String PICNIC_REPOSITORY = "picnic-errorprone";
public static final int ERRORPRONE_REPOSITORY_RULES_COUNT = 475;
- public static final int NULLAWAY_REPOSITORY_RULES_COUNT = 1;
+ public static final int NULLAWAY_REPOSITORY_RULES_COUNT = 2;
public static final int ERRORPRONE_SLF4J_REPOSITORY_RULES_COUNT = 8;
public static final int PICNIC_REPOSITORY_RULES_COUNT = 45;
diff --git a/sonar-erroraway-sonar-plugin/pom.xml b/sonar-erroraway-sonar-plugin/pom.xml
index 54de820..ea40894 100644
--- a/sonar-erroraway-sonar-plugin/pom.xml
+++ b/sonar-erroraway-sonar-plugin/pom.xml
@@ -236,7 +236,7 @@
org.sonarsource.sonar-packaging-maven-plugin
sonar-packaging-maven-plugin
- 1.21.0.505
+ 1.23.0.740
true
errorawaysonar
diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java
index 005b92f..41005b9 100644
--- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java
+++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java
@@ -15,7 +15,11 @@
*/
package com.github.erroraway.sonarqube;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import javax.tools.Diagnostic;
@@ -36,6 +40,8 @@
import com.github.erroraway.ErrorAwayException;
import com.github.erroraway.rules.ErrorAwayRulesMapping;
+import com.google.errorprone.BugCheckerInfo;
+import com.google.errorprone.bugpatterns.BugChecker;
/**
* @author Guillaume
@@ -44,6 +50,8 @@
public class ErrorAwayDiagnosticListener implements DiagnosticListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorAwayDiagnosticListener.class);
+ private static final Map PLUGIN_RULE_REPOSITORY_MAP = buildPluginRuleRepositoryMap();
+
public static final String ERROR_PRONE_COMPILER_CRASH_CODE = "compiler.err.error.prone.crash";
private static final Set ERROR_PRONE_DIAGNOSTIC_CODES = Set.of("compiler.warn.error.prone",
"compiler.err.error.prone",
@@ -62,7 +70,7 @@ public void report(Diagnostic extends JavaFileObject> diagnostic) {
String message = diagnostic.getMessage(Locale.ENGLISH);
String rule = parseRule(diagnostic, message);
- RuleKey ruleKey = RuleKey.of(findRepository(rule, message), rule);
+ RuleKey ruleKey = RuleKey.of(findRepository(rule), rule);
int startLine = (int) diagnostic.getLineNumber();
@@ -156,18 +164,20 @@ private String parseRule(Diagnostic extends JavaFileObject> diagnostic, String
}
}
- private String findRepository(String rule, String message) {
- if (rule.startsWith("Slf4j")) {
- return ErrorAwayRulesMapping.ERRORPRONE_SLF4J_REPOSITORY;
- }
-
- if (message.contains("see https://error-prone.picnic.tech/bugpatterns/")) {
- return ErrorAwayRulesMapping.PICNIC_REPOSITORY;
- }
+ private String findRepository(String rule) {
+ String repository = PLUGIN_RULE_REPOSITORY_MAP.get(rule);
+ return repository != null ? repository : ErrorAwayRulesMapping.ERRORPRONE_REPOSITORY;
+ }
- return switch (rule) {
- case "NullAway" -> ErrorAwayRulesMapping.NULLAWAY_REPOSITORY;
- default ->ErrorAwayRulesMapping.ERRORPRONE_REPOSITORY;
- };
+ private static Map buildPluginRuleRepositoryMap() {
+ Map map = new HashMap<>();
+ Iterator checkers = ErrorAwayRulesMapping.pluginCheckers();
+ while (checkers.hasNext()) {
+ BugChecker checker = checkers.next();
+ String repo = ErrorAwayRulesMapping.repository(checker.getClass());
+ String ruleKey = BugCheckerInfo.create(checker.getClass()).canonicalName();
+ map.put(ruleKey, repo);
+ }
+ return Collections.unmodifiableMap(map);
}
}
\ No newline at end of file
diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java
index 62f78cd..41d9821 100644
--- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java
+++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java
@@ -108,14 +108,19 @@ public void define(Context context) {
.build());
for (NullAwayOption option : NullAwayOption.values()) {
- context.addExtension(PropertyDefinition
+ PropertyDefinition.Builder builder = PropertyDefinition
.builder(option.getKey())
.name(option.getName())
.description(option.getDescription())
.category(PROPERTY_NULLAWAY_CATEGORY)
- .onQualifiers(Qualifiers.PROJECT)
- .multiValues(true)
- .build());
+ .onQualifiers(Qualifiers.PROJECT);
+
+ switch (option.getType()) {
+ case BOOLEAN -> builder.type(PropertyType.BOOLEAN);
+ case STRING_ARRAY -> builder.multiValues(true);
+ }
+
+ context.addExtension(builder.build());
}
context.addExtension(ErrorAwayRulesDefinition.class);
diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java
index 5848ae7..b822dba 100644
--- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java
+++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java
@@ -51,6 +51,7 @@
import com.github.erroraway.ErrorAwayException;
import com.github.erroraway.rules.ErrorAwayRulesMapping;
import com.google.errorprone.BugCheckerInfo;
+import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneJavaCompiler;
import com.google.errorprone.ErrorProneOptions;
import com.google.errorprone.bugpatterns.BugChecker;
@@ -92,7 +93,7 @@ public void execute(SensorContext context) {
}
// Compiler options
- ErrorProneOptions errorProneOptions = buildErrorProneOptions(context);
+ ErrorProneOptions errorProneOptions = buildErrorProneOptions(context, checkers);
List javacOptions = buildJavacOptions();
// Setup the compiler and analyze the code
@@ -121,14 +122,16 @@ public void execute(SensorContext context) {
}
}
- private ErrorProneOptions buildErrorProneOptions(SensorContext context) {
+ private ErrorProneOptions buildErrorProneOptions(SensorContext context, List> checkers) {
Configuration configuration = context.config();
List options = new ArrayList<>();
+ boolean onlyNullMarked = getBoolean(configuration, NullAwayOption.ONLY_NULL_MARKED);
+
// Fail here if the option was not set instead a showing a giant stacktrace
- if (!configuration.hasKey(NullAwayOption.ANNOTATED_PACKAGES.getKey())) {
+ if (!onlyNullMarked && !hasOption(configuration, NullAwayOption.ANNOTATED_PACKAGES)) {
if (context.activeRules().find(RuleKey.of("nullaway", "NullAway")) != null) {
- throw new ErrorAwayException("The " + NullAwayOption.ANNOTATED_PACKAGES.getKey() + " option must be set when the NullAway rule is enabled");
+ throw new ErrorAwayException("The " + NullAwayOption.ANNOTATED_PACKAGES.getKey() + " option must be set when the NullAway rule is enabled (unless " + NullAwayOption.ONLY_NULL_MARKED.getKey() + " is set)");
}
// When some annotation processors are enabled com.google.errorprone.ErrorPronePlugins turns on plugin
@@ -139,18 +142,68 @@ private ErrorProneOptions buildErrorProneOptions(SensorContext context) {
}
for (NullAwayOption option : NullAwayOption.values()) {
- String key = option.getKey();
+ // In OnlyNullMarked mode, skip package-scope options since NullAway would check
+ // both scopes if both are set
+ if (onlyNullMarked && option.isPackageScopeOption()) {
+ continue;
+ }
- if (configuration.hasKey(key)) {
- String[] values = configuration.getStringArray(key);
+ if (hasOption(configuration, option)) {
+ switch (option.getType()) {
+ case BOOLEAN -> {
+ if (getBoolean(configuration, option)) {
+ options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=true");
+ }
+ }
+ case STRING_ARRAY -> {
+ String[] values = getStringArray(configuration, option);
+ options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=" + String.join(",", values));
+ }
+ }
+ }
+ }
- options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=" + String.join(",", values));
+ // Override SUGGESTION-level checkers to WARN so they produce diagnostics.
+ // At SUGGESTION severity, ErrorProne attempts to generate auto-fix patches which requires
+ // com.sun.source.tree.Tree — inaccessible from the SonarQube plugin classloader.
+ // This crashes the entire compilation, suppressing all diagnostics from all checkers.
+ for (Class extends BugChecker> checker : checkers) {
+ BugCheckerInfo info = BugCheckerInfo.create(checker);
+ if (info.defaultSeverity() == BugPattern.SeverityLevel.SUGGESTION) {
+ options.add("-Xep:" + info.canonicalName() + ":WARN");
}
}
return ErrorProneOptions.processArgs(options);
}
+ /**
+ * Checks if an option is set, looking at both the server-side key and the
+ * analysis parameter key (sonar. prefixed).
+ */
+ private boolean hasOption(Configuration configuration, NullAwayOption option) {
+ return configuration.hasKey(option.getKey()) || configuration.hasKey(option.getAnalysisParameterKey());
+ }
+
+ /**
+ * Reads a boolean option, checking both the server-side key and the analysis parameter key.
+ */
+ private boolean getBoolean(Configuration configuration, NullAwayOption option) {
+ return configuration.getBoolean(option.getKey()).orElse(false)
+ || configuration.getBoolean(option.getAnalysisParameterKey()).orElse(false);
+ }
+
+ /**
+ * Reads a string array option, preferring the analysis parameter key over the server-side key.
+ */
+ private String[] getStringArray(Configuration configuration, NullAwayOption option) {
+ String[] values = configuration.getStringArray(option.getAnalysisParameterKey());
+ if (values != null && values.length > 0) {
+ return values;
+ }
+ return configuration.getStringArray(option.getKey());
+ }
+
private List buildJavacOptions() {
List options = new ArrayList<>();
@@ -220,9 +273,8 @@ private void addErrorProneCheckers(SensorContext context, List