Skip to content

Commit 8a83a17

Browse files
authored
fix: sonar exclusions (#52)
* introducing sonar exclusions * Adding configuration parameter to be able to skip including sonar patterns * Fixing tests * `SonarExclusions` fully under test now * Trying to improve the test * remove the debug log entries
1 parent af62982 commit 8a83a17

File tree

5 files changed

+1019
-26
lines changed

5 files changed

+1019
-26
lines changed

jacoco-console-reporter/src/main/java/io/github/svaningelgem/JacocoConsoleReporterMojo.java

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@
2525
* This plugin provides a simple way to view coverage metrics directly in the console
2626
* without needing to generate HTML or XML reports.
2727
*/
28-
@Mojo(
29-
name = "report",
30-
defaultPhase = LifecyclePhase.VERIFY,
31-
threadSafe = true
32-
)
28+
@Mojo(name = "report", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true)
3329
public class JacocoConsoleReporterMojo extends AbstractMojo {
3430
private final Pattern PACKAGE_PATTERN = Pattern.compile("(?:^|\\*/)\\s*package\\s+([^;]+);", Pattern.DOTALL | Pattern.MULTILINE);
3531

@@ -105,6 +101,12 @@ public class JacocoConsoleReporterMojo extends AbstractMojo {
105101
@Parameter(defaultValue = "true", property = "ignoreFilesInBuildDirectory")
106102
boolean ignoreFilesInBuildDirectory;
107103

104+
/**
105+
* When true, ignore the files in the build directory. 99.9% of the time these are automatically generated files.
106+
*/
107+
@Parameter(defaultValue = "true", property = "interpretSonarIgnorePatterns")
108+
boolean interpretSonarIgnorePatterns;
109+
108110
/**
109111
* Base directory for compiled output.
110112
*/
@@ -139,6 +141,7 @@ public class JacocoConsoleReporterMojo extends AbstractMojo {
139141
static final Set<File> collectedExecFilePaths = new HashSet<>();
140142
static final Set<File> collectedClassesPaths = new HashSet<>();
141143
static final Set<Pattern> collectedExcludePatterns = new HashSet<>();
144+
static final Set<SonarExclusionPattern> collectedSonarExcludePatterns = new HashSet<>();
142145

143146
FileReader fileReader = new FileReader();
144147

@@ -172,6 +175,7 @@ public void execute() throws MojoExecutionException {
172175
void loadExclusionPatterns() {
173176
addBuildDirExclusion();
174177
addJacocoExclusions();
178+
addSonarExclusions();
175179
}
176180

177181
/**
@@ -230,6 +234,46 @@ void addJacocoExclusions() {
230234
});
231235
}
232236

237+
/**
238+
* Extracts exclusion patterns from Sonar properties
239+
*/
240+
void addSonarExclusions() {
241+
if (!interpretSonarIgnorePatterns) return;
242+
243+
// Check for Sonar exclusions in project properties
244+
Properties projectProperties = project.getProperties();
245+
246+
// Handle sonar.exclusions (file-based patterns)
247+
String sonarExclusions = projectProperties.getProperty("sonar.exclusions");
248+
if (sonarExclusions != null && !sonarExclusions.trim().isEmpty()) {
249+
addSonarFileExclusions(sonarExclusions);
250+
}
251+
252+
// Handle sonar.coverage.exclusions (file-based patterns)
253+
String sonarCoverageExclusions = projectProperties.getProperty("sonar.coverage.exclusions");
254+
if (sonarCoverageExclusions != null && !sonarCoverageExclusions.trim().isEmpty()) {
255+
addSonarFileExclusions(sonarCoverageExclusions);
256+
}
257+
}
258+
259+
/**
260+
* Processes Sonar file-based exclusions and stores them for later evaluation
261+
*/
262+
void addSonarFileExclusions(@NotNull String exclusions) {
263+
String[] patterns = exclusions.split(",");
264+
for (String pattern : patterns) {
265+
pattern = pattern.trim();
266+
if (pattern.isEmpty()) {
267+
continue;
268+
}
269+
270+
// Store the original pattern with project context for later evaluation
271+
SonarExclusionPattern sonarPattern = new SonarExclusionPattern(pattern, project);
272+
collectedSonarExcludePatterns.add(sonarPattern);
273+
getLog().debug("Added Sonar file exclusion pattern: " + pattern);
274+
}
275+
}
276+
233277
/**
234278
* Converts a JaCoCo exclude pattern to a Java regex Pattern
235279
* <p/>
@@ -247,17 +291,13 @@ Pattern convertExclusionToPattern(@NotNull String jacocoPattern) {
247291
}
248292

249293
// Use temporary placeholders to avoid interference between replacements
250-
String regex = jacocoPattern
251-
.replace("**/", "__DOUBLE_STAR__") // Any directory
294+
String regex = jacocoPattern.replace("**/", "__DOUBLE_STAR__") // Any directory
252295
.replace("**", "__DOUBLE_STAR__") // Any directory
253296
.replace("*", "__STAR__") // Any character (but not a directory)
254297
.replace(".", "__DOT__");
255298

256299
// Now perform the actual replacements
257-
regex = regex
258-
.replace("__DOUBLE_STAR__", "(?:[^/]*/)*")
259-
.replace("__STAR__", "[^/]*")
260-
.replace("__DOT__", "\\.");
300+
regex = regex.replace("__DOUBLE_STAR__", "(?:[^/]*/)*").replace("__STAR__", "[^/]*").replace("__DOT__", "\\.");
261301

262302
regex = "^" + regex + "$";
263303

@@ -272,14 +312,34 @@ void addExclusion(@NotNull String jacocoPattern) {
272312
}
273313

274314
/**
275-
* Checks if a class should be excluded based on its name
315+
* Checks if a class should be excluded based on its name and file path
276316
*/
277317
boolean isExcluded(String className) {
278-
if (collectedExcludePatterns.isEmpty()) {
279-
return false;
318+
return isExcluded(className, null);
319+
}
320+
321+
/**
322+
* Checks if a class should be excluded based on its name and optional file path
323+
*/
324+
boolean isExcluded(String className, String filePath) {
325+
// Check JaCoCo-style package exclusions
326+
if (!collectedExcludePatterns.isEmpty()) {
327+
if (collectedExcludePatterns.stream().anyMatch(p -> p.matcher(className).matches())) {
328+
return true;
329+
}
330+
}
331+
332+
// Check Sonar-style file exclusions if we have a file path
333+
if (filePath != null && !collectedSonarExcludePatterns.isEmpty()) {
334+
for (SonarExclusionPattern sonarPattern : collectedSonarExcludePatterns) {
335+
if (sonarPattern.matches(filePath, project)) {
336+
getLog().debug("Excluded by Sonar pattern '" + sonarPattern.getOriginalPattern() + "': " + filePath);
337+
return true;
338+
}
339+
}
280340
}
281341

282-
return collectedExcludePatterns.stream().anyMatch(p -> p.matcher(className).matches());
342+
return false;
283343
}
284344

285345
void generateReport() throws MojoExecutionException {
@@ -546,13 +606,21 @@ void printTree(@NotNull DirectoryNode root) {
546606
void buildDirectoryTreeAddNode(DirectoryNode root, @NotNull IPackageCoverage packageCoverage, @NotNull ISourceFileCoverage sourceFileCoverage) {
547607
String filename = sourceFileCoverage.getName();
548608
String className = filename.substring(0, filename.lastIndexOf('.'));
609+
String packageName = packageCoverage.getName();
610+
String classPath = packageName + "/" + className;
549611

550-
String p = packageCoverage.getName() + "/" + className;
551-
if (isExcluded(p)) {
612+
// Construct potential file paths for Sonar pattern matching
613+
String javaFilePath = packageName.replace("/", "/") + "/" + filename;
614+
String srcMainJavaPath = "src/main/java/" + javaFilePath;
615+
String srcTestJavaPath = "src/test/java/" + javaFilePath;
616+
617+
// Check exclusions with both package-style and file-style paths
618+
if (isExcluded(classPath) || isExcluded(classPath, javaFilePath) || isExcluded(classPath, srcMainJavaPath) || isExcluded(classPath, srcTestJavaPath)) {
619+
getLog().debug("Excluded source file: " + javaFilePath);
552620
return;
553621
}
554622

555-
String[] pathComponents = packageCoverage.getName().split("/");
623+
String[] pathComponents = packageName.split("/");
556624
DirectoryNode current = root;
557625
for (String component : pathComponents) {
558626
current = current.getSubdirectories().computeIfAbsent(component, DirectoryNode::new);
@@ -577,7 +645,6 @@ void buildDirectoryTreeAddNode(DirectoryNode root, @NotNull IPackageCoverage pac
577645
metrics.setCoveredBranches(sourceFileCoverage.getBranchCounter().getCoveredCount());
578646

579647
current.getSourceFiles().add(new SourceFileNode(sourceFileName, metrics));
580-
581648
}
582649

583650
void buildDirectoryTreeAddNode(DirectoryNode root, @NotNull IPackageCoverage packageCoverage) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.github.svaningelgem;
2+
3+
import lombok.Data;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.apache.maven.project.MavenProject;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
import java.io.File;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.util.regex.Pattern;
13+
14+
/**
15+
* Represents a Sonar exclusion pattern with its source project context
16+
*/
17+
@Data
18+
@RequiredArgsConstructor
19+
public class SonarExclusionPattern {
20+
/**
21+
* The original Sonar pattern (file-based)
22+
*/
23+
private final String originalPattern;
24+
25+
/**
26+
* The project this pattern was found in
27+
*/
28+
private final MavenProject sourceProject;
29+
30+
/**
31+
* Compiled regex pattern for matching
32+
*/
33+
private Pattern compiledPattern;
34+
35+
/**
36+
* Checks if a file path (relative to the source project) matches this exclusion pattern
37+
*/
38+
public boolean matches(@NotNull String filePath, @NotNull MavenProject currentProject) {
39+
if (compiledPattern == null) {
40+
compiledPattern = compilePattern();
41+
}
42+
43+
// Convert the file path to be relative to the source project if needed
44+
String relativePath = getRelativePath(filePath, currentProject);
45+
46+
return compiledPattern.matcher(relativePath).matches();
47+
}
48+
49+
/**
50+
* Gets the file path relative to the source project
51+
*/
52+
@NotNull String getRelativePath(@NotNull String filePath, @NotNull MavenProject currentProject) {
53+
if (currentProject.equals(sourceProject)) {
54+
return filePath;
55+
}
56+
57+
try {
58+
File sourceBaseDir = sourceProject.getBasedir();
59+
File currentBaseDir = currentProject.getBasedir();
60+
61+
if (sourceBaseDir != null && currentBaseDir != null) {
62+
Path sourcePath = sourceBaseDir.toPath();
63+
Path currentPath = currentBaseDir.toPath();
64+
Path relativePath = sourcePath.relativize(currentPath);
65+
66+
return relativePath.resolve(filePath).toString().replace('\\', '/');
67+
}
68+
} catch (Exception e) {
69+
// Fall back to original path if relativization fails
70+
}
71+
72+
return filePath;
73+
}
74+
75+
/**
76+
* Compiles the Sonar pattern into a regex Pattern
77+
*/
78+
@NotNull Pattern compilePattern() {
79+
String pattern = originalPattern.replace("\\", "/");
80+
81+
// Escape regex special characters except for our wildcards
82+
pattern = pattern
83+
.replace(".", "\\.")
84+
.replace("(", "\\(")
85+
.replace(")", "\\)")
86+
.replace("[", "\\[")
87+
.replace("]", "\\]")
88+
.replace("{", "\\{")
89+
.replace("}", "\\}")
90+
.replace("^", "\\^")
91+
.replace("$", "\\$")
92+
.replace("+", "\\+")
93+
.replace("|", "\\|");
94+
95+
// Handle wildcards with proper precedence - use placeholders to avoid interference
96+
// First handle **/ (directory wildcard)
97+
pattern = pattern.replace("**/", "__DOUBLE_STAR_SLASH__");
98+
99+
// Then handle trailing ** (matches everything remaining)
100+
pattern = pattern.replaceAll("\\*\\*$", "__TRAILING_DOUBLE_STAR__");
101+
102+
// Handle remaining ** in the middle (treat as directory wildcard)
103+
pattern = pattern.replace("**", "__DOUBLE_STAR_SLASH__");
104+
105+
// Handle single * (filename wildcard)
106+
pattern = pattern.replace("*", "__STAR__");
107+
108+
// Convert placeholders to regex
109+
pattern = pattern
110+
.replace("__DOUBLE_STAR_SLASH__", "(?:[^/]*/)*")
111+
.replace("__TRAILING_DOUBLE_STAR__", "(?:[^/]*/)*(?:[^/]*)")
112+
.replace("__STAR__", "[^/]*");
113+
114+
// Anchor the pattern
115+
pattern = "^" + pattern + "$";
116+
117+
return Pattern.compile(pattern);
118+
}
119+
}

jacoco-console-reporter/src/test/java/io/github/svaningelgem/BaseTestClass.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public void setUp() throws Exception {
9393
mojo.weightBranchCoverage = 0.4;
9494
mojo.weightLineCoverage = 0.4;
9595
mojo.ignoreFilesInBuildDirectory = true;
96+
mojo.interpretSonarIgnorePatterns = true;
9697
mojo.targetDir = new File(project.getBuild().getDirectory()).getCanonicalFile();
9798
mojo.baseDir = project.getBasedir();
9899

@@ -105,6 +106,7 @@ public void tearDown() {
105106
JacocoConsoleReporterMojo.collectedClassesPaths.clear();
106107
JacocoConsoleReporterMojo.collectedExecFilePaths.clear();
107108
JacocoConsoleReporterMojo.collectedExcludePatterns.clear();
109+
JacocoConsoleReporterMojo.collectedSonarExcludePatterns.clear();
108110
}
109111

110112
private static int nextInt(int bound) {
@@ -129,6 +131,15 @@ private static int nextInt(int bound) {
129131
return cm;
130132
}
131133

134+
protected void assertLogNotContains(@NotNull String @NotNull [] expected) {
135+
try {
136+
assertLogContains(expected);
137+
} catch (AssertionError e) {
138+
return; // Good!
139+
}
140+
141+
failLog(expected, "Expected log to NOT contain:");
142+
}
132143
protected void assertLogContains(@NotNull String @NotNull [] expected) {
133144
assertTrue("Wrong test: we need SOMETHING to check!", expected.length > 0);
134145

@@ -157,8 +168,12 @@ protected void assertLogContains(@NotNull String @NotNull [] expected) {
157168
}
158169

159170
protected void failLog(String @NotNull [] expected) {
171+
failLog(expected, "Expected log to contain:");
172+
}
173+
174+
protected void failLog(String @NotNull [] expected, String message) {
160175
StringBuilder builder = new StringBuilder();
161-
builder.append("Expected log to contain:\n");
176+
builder.append(message).append('\n');
162177
for (String line : expected) {
163178
builder.append(line).append("\n");
164179
}

jacoco-console-reporter/src/test/java/io/github/svaningelgem/BuildDirectoryTreeExclusionTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,12 @@ public void testBuildDirectoryTreeWithExcludedFiles() throws Exception {
7171
"[info] com.example ",
7272
"[info] └─IncludedClass.java"
7373
};
74+
String[] notExpected = {
75+
"[info] └─ExcludedClass.java"
76+
};
7477

7578
assertLogContains(expected);
76-
77-
// Verify that the excluded file is not in the log
78-
for (String line : log.writtenData) {
79-
assertFalse("Log should not contain ExcludedClass",
80-
line.contains("ExcludedClass") && !line.contains("Converted pattern"));
81-
}
79+
assertLogNotContains(notExpected);
8280
}
8381

8482
/**

0 commit comments

Comments
 (0)