I wanted to add a new custom language check. The language is called Gherkin, whereas the file suffix is .feature. I let Microsoft’s copilot generate some Java classes and a pom, and added some logs.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sonar-gherkin-plugin</artifactId>
<version>1.0.0</version>
<packaging>sonar-plugin</packaging>
<dependencies>
<dependency>
<groupId>org.sonarsource.api.plugin</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>10.14.0.2599</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<maven.compiler.encoding>${project.build.sourceEncoding}</maven.compiler.encoding>
<maven.resources.encoding>${project.build.sourceEncoding}</maven.resources.encoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.23.0.740</version>
<extensions>true</extensions>
<configuration>
<pluginKey>gherkin-rules</pluginKey>
<pluginName>Gherkin Rules Plugin</pluginName>
<pluginClass>com.example.gherkin.GherkinRulesPlugin</pluginClass>
<pluginDescription>my first attempts to implement Gherkin checks</pluginDescription>
<requiredForLanguages>gherkin</requiredForLanguages>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.example.gherkin;
import org.sonar.api.resources.Language;
public class GherkinLanguage implements Language {
public static final String LANGUAGE_KEY = "gherkin";
public static final String LANGUAGE_NAME = "Gherkin";
@Override
public String getKey() {
System.out.println("GherkinLanguage.getKey " + LANGUAGE_KEY);
return LANGUAGE_KEY;
}
@Override
public String getName() {
System.out.println("GherkinLanguage.getName " + LANGUAGE_NAME);
return LANGUAGE_NAME;
}
@Override
public String[] getFileSuffixes() {
System.out.println("GherkinLanguage.getFileSuffixes");
return new String[]{".feature"};
}
}
package com.example.gherkin;
import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
public class GherkinQualityProfile implements BuiltInQualityProfilesDefinition {
@Override
public void define(Context context) {
System.out.println(">GherkinQualityProfile define");
NewBuiltInQualityProfile profile = context
.createBuiltInQualityProfile("Gherkin Way", GherkinLanguage.LANGUAGE_KEY);
profile.setDefault(true);
profile.activateRule("gherkin-rules", "scenario-case");
profile.done();
System.out.println("<GherkinQualityProfile define");
}
}
package com.example.gherkin;
import org.sonar.api.Plugin;
public class GherkinRulesPlugin implements Plugin {
@Override
public void define(Context context) {
System.out.println(">GherkinRulesPlugin.define");
context.addExtensions(
GherkinLanguage.class,
ScenarioCaseCheck.class,
GherkinQualityProfile.class,
GherkinSensor.class);
System.out.println("<GherkinRulesPlugin.define");
}
}
package com.example.gherkin;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import java.io.IOException;
import java.util.List;
import java.util.SortedSet;
public class GherkinSensor implements Sensor {
private static final String SCENARIO_KEY = "Scenario:";
private static final int SCENARIO_KEY_LENGTH = SCENARIO_KEY.length();
@Override
public void describe(SensorDescriptor sensorDescriptor) {
System.out.println(">GherkinSensor.describe");
sensorDescriptor
.name("Gherkin Sensor")
.onlyOnLanguage(GherkinLanguage.LANGUAGE_KEY)
.onlyOnFileType(InputFile.Type.MAIN)
.createIssuesForRuleRepository(GherkinLanguage.LANGUAGE_KEY + "-Scenario");
System.out.println("<GherkinSensor.describe");
}
@Override
public void execute(SensorContext context) {
System.out.println(">GherkinSensor.execute");
FileSystem fs = context.fileSystem();
SortedSet<String> languages = fs.languages();
for (String language : languages) {
System.out.println("Gherkin: " + language);
}
System.out.println(String.format("Gherkin start to scan file system %s", fs));
Iterable<InputFile> featureFiles = fs.inputFiles(fs.predicates().hasLanguage(GherkinLanguage.LANGUAGE_KEY));
for (InputFile inputFile : featureFiles) {
System.out.println(String.format("scanning %s", inputFile.filename()));
try {
List<String> lines = inputFile.contents().lines().toList();
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.startsWith(SCENARIO_KEY)) {
String scenarioText = trimmed.substring(SCENARIO_KEY_LENGTH).trim();
if (!scenarioText.isEmpty() && !Character.isUpperCase(scenarioText.charAt(0))) {
System.out.println(scenarioText);
NewIssue issue = context.newIssue()
.forRule(ScenarioCaseCheck.RULE_KEY)
// .at(inputFile.newPointer(i + 1, 0))
;
issue.save();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.println("<GherkinSensor.execute");
}
}
package com.example.gherkin;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.rule.RulesDefinition;
public class ScenarioCaseCheck implements RulesDefinition {
public static final RuleKey RULE_KEY = RuleKey.of("gherkin-rules", "scenario-case");
@Override
public void define(Context context) {
System.out.println(">GherkinRulesPlugin.define");
NewRepository repo = context
.createRepository(RULE_KEY.repository(), GherkinLanguage.LANGUAGE_KEY)
.setName("Gherkin Rules");
repo.createRule(RULE_KEY.rule())
.setName("Scenario must start with capital letter")
.setHtmlDescription("Scenarios should begin with a capital letter to maintain readability.");
repo.done();
System.out.println("<GherkinRulesPlugin.define");
}
}
I could create the jar and deploy it into the SonarQube server. When I started it, the log showed only that gherkinRulesPlugin.define was invoked three times and gherkinLanguage.getKey many times and GherkinLanguage.getName some times. In the global configuration of Sonar, I also added the file pattern **/*.feature to be analyzed.
I then added the Quality profile to one of my projects. For that project, I started mvn sonar:sonar. In the server log, GherkinLanguage.getKey was invoked quite some times. But the new language was not checked at all. The log of the mvn command is this:
[INFO] --- sonar:3.10.0.2594:sonar (default-cli) @ star-en-ex ---
[INFO] User cache: C:\Users\C33016\.sonar\cache
[INFO] SonarQube version: 25.4.0.105899
[INFO] Default locale: "de_DE", source code encoding: "UTF-8"
[INFO] Load global settings
[INFO] Load global settings (done) | time=392ms
[INFO] Server id: 63D9CE3B-AYpFXXY47thQMFK43cPX
[INFO] Loading required plugins
[INFO] Load plugins index
[INFO] Load plugins index (done) | time=299ms
[INFO] Load/download plugins
[INFO] Load/download plugins (done) | time=93ms
[INFO] Process project properties
[INFO] Process project properties (done) | time=61ms
[INFO] Project key: starenex
[INFO] Base dir: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[INFO] Working dir: xxxxxxxxxxxxxxxxxxxxx
[INFO] Load project settings for component key: 'starenex'
[INFO] Load project settings for component key: 'starenex' (done) | time=246ms
[INFO] Load quality profiles
[INFO] Load quality profiles (done) | time=293ms
[INFO] Load active rules
[INFO] Load active rules (done) | time=740ms
[INFO] Load analysis cache
[INFO] Load analysis cache (5.3 kB) | time=178ms
[INFO] Preprocessing files...
[INFO] 2 languages detected in 121 preprocessed files
[INFO] 0 files ignored because of inclusion/exclusion patterns
[INFO] 0 files ignored because of scm ignore settings
[INFO] Loading plugins for detected languages
[INFO] Load/download plugins
[INFO] Load/download plugins (done) | time=113ms
[INFO] Load project repositories
[INFO] Load project repositories (done) | time=575ms
[INFO] Indexing files...
[INFO] Project configuration:
[INFO] Included sources: **/*.java, **/*.xml, **/*.feature
[INFO] Excluded sources for coverage: **/**
[INFO] 121 files indexed
[INFO] Quality profile for java: Eon Test Automation
[INFO] Quality profile for xml: Sonar way
[INFO] ------------- Run sensors on module Star EnEx
[INFO] Load metrics repository
[INFO] Load metrics repository (done) | time=60ms
[INFO] Sensor JavaSensor [java]
[INFO] Configured Java source version (sonar.java.source): 17, preview features enabled (sonar.java.enablePreview): false
[INFO] Server-side caching is enabled. The Java analyzer will not try to leverage data from a previous analysis.
[INFO] Using ECJ batch to parse 85 Main java source files with batch size 426 KB.
[INFO] Starting batch processing.
[INFO] The Java analyzer cannot skip unchanged files in this context. A full analysis is performed for all files.
[INFO] 58% analyzed
[WARNING] import for com.eon.star.framework.entity.playwright.PlaywrightContextHolder found
[INFO] field powercloudLoginViaSteps of class StartSessionSteps skipped because type PowercloudLoginViaSteps in exclusions
[INFO] 100% analyzed
[INFO] Batch processing: Done.
[INFO] Did not optimize analysis for any files, performed a full analysis for all 85 files.
[INFO] Using ECJ batch to parse 35 Test java source files with batch size 426 KB.
[INFO] Starting batch processing.
[INFO] 100% analyzed
[INFO] Batch processing: Done.
[INFO] Did not optimize analysis for any files, performed a full analysis for all 35 files.
[INFO] No "Generated" source files to scan.
[INFO] Sensor JavaSensor [java] (done) | time=23441ms
[INFO] Sensor SurefireSensor [java]
[INFO] parsing [C:\Users\C33016\IdeaProjects\star-en-ex\target\surefire-reports]
[INFO] Sensor SurefireSensor [java] (done) | time=226ms
[INFO] Sensor XML Sensor [xml]
[INFO] 1 source file to be analyzed
[INFO] 1/1 source file has been analyzed
[INFO] Sensor XML Sensor [xml] (done) | time=270ms
[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] 'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml
[INFO] No report imported, no coverage information will be imported by JaCoCo XML Report Importer
[INFO] Sensor JaCoCo XML Report Importer [jacoco] (done) | time=6ms
[INFO] Sensor Java Config Sensor [iac]
[INFO] 0 source files to be analyzed
[INFO] 0/0 source files have been analyzed
[INFO] Sensor Java Config Sensor [iac] (done) | time=41ms
[INFO] Sensor IaC Docker Sensor [iac]
[INFO] 0 source files to be analyzed
[INFO] 0/0 source files have been analyzed
[INFO] Sensor IaC Docker Sensor [iac] (done) | time=144ms
[INFO] Sensor TextAndSecretsSensor [text]
[INFO] Available processors: 12
[INFO] Using 12 threads for analysis.
[INFO] Using git CLI to retrieve untracked files
[INFO] Analyzing language associated files and files included via "sonar.text.inclusions" that are tracked by git
[INFO] 121 source files to be analyzed
[INFO] 121/121 source files have been analyzed
[INFO] Sensor TextAndSecretsSensor [text] (done) | time=1805ms
[INFO] ------------- Run sensors on project
[INFO] Sensor Zero Coverage Sensor
[INFO] Sensor Zero Coverage Sensor (done) | time=5ms
[INFO] Sensor Java CPD Block Indexer
[INFO] Sensor Java CPD Block Indexer (done) | time=161ms
[INFO] ------------- Gather SCA dependencies on project
[INFO] Dependency analysis skipped
[INFO] CPD Executor 11 files had no CPD blocks
[INFO] CPD Executor Calculating CPD for 74 files
[INFO] CPD Executor CPD calculation finished (done) | time=85ms
[INFO] SCM revision ID '869af4d2b5daaf0f29de42103a16dee0b05119dc'
[INFO] Analysis report generated in 430ms, dir size=773.6 kB
[INFO] Analysis report compressed in 997ms, zip size=335.8 kB
[INFO] Analysis report uploaded in 353ms
[INFO] ------------- Check Quality Gate status
[INFO] Waiting for the analysis report to be processed (max 300s)
[INFO] QUALITY GATE STATUS: PASSED - View details on http://localhost:9100/dashboard?id=starenex
[INFO] Analysis total time: 56.123 s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:12 min
[INFO] Finished at: 2025-07-10T08:52:09+02:00
[INFO] ------------------------------------------------------------------------
What I don’t understand: why is the gherkin sensor not invoked at all, and why does the log tell “2 languages detected…” and not 3?