Custom Language's sensor not triggered

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?

Hello,

Have you checked out SonarQube’s custom plugin example? Modeling your implementation after this example might be more effective than relying solely on Copilot for guidance.

I built the custom plugin example, and it showed the same effects: the Sensors are not invoked. And mvn sonar:sonar responds that it only discovered 2 languages: java and xml. But it did not mention foo what I would have expected with the Sonar example

Of course, I have added Foo quality profile to my project

eventually, I got the foo example running by adding the **/*.foo to the global administration for the file extensions to be checked. That is a good starting point, and I’ll try my own things from that. Many thanks for your hint

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.