Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 86 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ java -jar linter-cli/target/linter-cli-0.1.2.jar \
| `--html` | Generate HTML report |
| `--json` | Generate JSON report |
| `--report-path <dir>` | Custom report directory |
| `--exclusions <file>` | Path to a JSON exclusion config (see [Excluding Issues](#excluding-issues)) |
| `--verbose` | Verbose logging |
| `--no-color` | Disable colored output (default: enabled) |
| `--no-fail` | Exit 0 even on errors |
Expand All @@ -135,6 +136,83 @@ java -jar linter-cli/target/linter-cli-0.1.2.jar \
| `TERM=dumb` | Disables colored output |
| `WT_SESSION`, `ANSICON` | Windows color detection |

## Excluding Issues

Users often encounter known, intentional, or external findings that clutter reports and make triage harder. The exclusion system lets you suppress specific lint items from HTML and JSON reports without modifying the plugin source.

### Configuration file

Create a file named **`dsf-linter-exclusions.json`** in the project root (auto-discovered) or point to it explicitly with `--exclusions`:

```json
{
"affectsExitStatus": false,
"rules": [
{ "type": "BPMN_PROCESS_HISTORY_TIME_TO_LIVE_MISSING" },
{ "severity": "WARN", "file": "update-allow-list.bpmn" },
{ "messageContains": "optional field" }
]
}
```

### Rule fields

Each rule is an **AND** combination of its non-null fields. Multiple rules are **OR**-combined — an item is excluded when *any* rule matches.

| Field | Match type | Example |
|---|---|---|
| `type` | Exact (case-insensitive) match against the `LintingType` enum name | `"BPMN_PROCESS_HISTORY_TIME_TO_LIVE_MISSING"` |
| `severity` | Exact (case-insensitive) match against the severity level | `"WARN"`, `"ERROR"`, `"INFO"` |
| `file` | Case-insensitive substring match against the file name | `"update-allow-list"` |
| `messageContains` | Case-insensitive substring match against the issue description | `"optional field"` |

Every rule must specify at least one field — a rule with no criteria is rejected on load.

### Exit-status control

| `affectsExitStatus` | Behaviour |
|---|---|
| `false` *(default)* | Excluded items are fully suppressed — they do **not** appear in reports and do **not** count towards the exit code |
| `true` | Excluded items are hidden from reports, but their error count **still** contributes to the exit code (non-zero exit on errors) |

### Usage examples

```bash

# Explicit exclusion file
java -jar linter-cli-0.1.2.jar \
--path plugin.jar --html \
--exclusions /path/to/my-exclusions.json

# Exclude only from reports, but still fail on excluded errors
```

Exclusion file with `affectsExitStatus: true`:
```json
{
"affectsExitStatus": true,
"rules": [
{ "type": "BPMN_PROCESS_HISTORY_TIME_TO_LIVE_MISSING" }
]
}
```

### Available `LintingType` values

All available type names are defined in `LintingType.java`. A few common examples:

| Type | Description |
|---|---|
| `BPMN_PROCESS_HISTORY_TIME_TO_LIVE_MISSING` | `historyTimeToLive` not set on process |
| `BPMN_PROCESS_NOT_EXECUTABLE` | Process `isExecutable` not set to `true` |
| `STRUCTURE_DEFINITION_SNAPSHOT_PRESENT` | StructureDefinition should not contain a snapshot |
| `FHIR_TASK_STATUS_NOT_DRAFT` | Task status is not `draft` |
| `PLUGIN_DEFINITION_MISSING_SERVICE_LOADER_REGISTRATION` | Plugin missing ServiceLoader registration |

See `linter-core/src/main/java/dev/dsf/linter/output/LintingType.java` for the full list.

---

## Project Structure

```
Expand All @@ -143,6 +221,7 @@ dsf-linter/
│ ├── src/main/java/dev/dsf/linter/
│ │ ├── analysis/ # Resource analysis
│ │ ├── bpmn/ # BPMN parsing & validation
│ │ ├── exclusion/ # Exclusion rules & filtering
│ │ ├── fhir/ # FHIR parsing & validation
│ │ ├── service/ # Linting services (BPMN, FHIR, Plugin)
│ │ ├── output/ # Lint item definitions
Expand Down Expand Up @@ -189,10 +268,11 @@ dsf-linter/
### Testing

```bash
mvn test # All tests
mvn test -Dtest=BpmnLoadingTest # Specific test
mvn test -X # Verbose
mvn clean package -DskipTests # Build without tests
mvn test # All tests
mvn test -Dtest=BpmnLoadingTest # Specific test
mvn test -Dtest=ExclusionFilterTest # Exclusion filter tests
mvn test -X # Verbose
mvn clean package -DskipTests # Build without tests
```

### Development Workflow
Expand Down Expand Up @@ -237,6 +317,8 @@ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 \
| `FhirLintingService` | FHIR validation |
| `PluginLintingService` | Plugin validation |
| `LintingReportGenerator` | Report generation |
| `ExclusionFilter` | Suppresses lint items matched by exclusion rules |
| `ExclusionConfigLoader` | Loads `dsf-linter-exclusions.json` |

## Report Output

Expand Down
15 changes: 10 additions & 5 deletions linter-cli/src/main/java/dev/dsf/linter/LinterExecutor.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.dsf.linter;

import dev.dsf.linter.exclusion.ExclusionConfig;
import dev.dsf.linter.logger.Logger;

import java.nio.file.Path;
Expand All @@ -22,26 +23,29 @@ public class LinterExecutor {
private final boolean generateHtmlReport;
private final boolean generateJsonReport;
private final boolean failOnErrors;
private final ExclusionConfig exclusionConfig;
private final Logger logger;

/**
* Constructs a new LinterExecutor with the specified parameters.
*
* @param projectPath the path to the project to lint
* @param reportPath the path where reports should be generated
* @param projectPath the path to the project to lint
* @param reportPath the path where reports should be generated
* @param generateHtmlReport whether to generate an HTML report
* @param generateJsonReport whether to generate a JSON report
* @param failOnErrors whether to fail (exit code 1) if errors are found
* @param logger the logger for output
* @param failOnErrors whether to fail (exit code 1) if errors are found
* @param exclusionConfig optional exclusion config; {@code null} disables exclusions
* @param logger the logger for output
*/
public LinterExecutor(Path projectPath, Path reportPath,
boolean generateHtmlReport, boolean generateJsonReport,
boolean failOnErrors, Logger logger) {
boolean failOnErrors, ExclusionConfig exclusionConfig, Logger logger) {
this.projectPath = projectPath;
this.reportPath = reportPath;
this.generateHtmlReport = generateHtmlReport;
this.generateJsonReport = generateJsonReport;
this.failOnErrors = failOnErrors;
this.exclusionConfig = exclusionConfig;
this.logger = logger;
}

Expand All @@ -63,6 +67,7 @@ public DsfLinter.OverallLinterResult execute() throws Exception {
generateHtmlReport,
generateJsonReport,
failOnErrors,
exclusionConfig,
logger
);

Expand Down
34 changes: 34 additions & 0 deletions linter-cli/src/main/java/dev/dsf/linter/Main.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.dsf.linter;

import dev.dsf.linter.exclusion.ExclusionConfig;
import dev.dsf.linter.exclusion.ExclusionConfigLoader;
import dev.dsf.linter.input.InputResolver;
import dev.dsf.linter.logger.ConsoleLogger;
import dev.dsf.linter.logger.Logger;
Expand Down Expand Up @@ -79,6 +81,13 @@ public class Main implements Callable<Integer> {
description = "Disable colored console output. (Default: enabled)")
private boolean disableColor = false;

@Option(names = "--exclusions",
description = "Path to a JSON file containing exclusion rules. "
+ "Excluded items are hidden from reports. "
+ "If omitted, the linter also looks for 'dsf-linter-exclusions.json' "
+ "in the project root automatically.")
private Path exclusionsFile = null;


/**
* Main entry point for the DSF Linter CLI application.
Expand Down Expand Up @@ -177,6 +186,30 @@ public Integer call() {
return 1;
}

// Load exclusion config (explicit file wins over auto-discovery)
ExclusionConfig exclusionConfig = null;
try {
ExclusionConfigLoader exclusionLoader = new ExclusionConfigLoader();
if (exclusionsFile != null) {
exclusionConfig = exclusionLoader.load(exclusionsFile);
logger.info("Loaded exclusion rules from: " + exclusionsFile);
} else {
Optional<ExclusionConfig> auto = exclusionLoader.loadFromProjectRoot(projectPath);
if (auto.isPresent()) {
exclusionConfig = auto.get();
logger.info("Auto-discovered exclusion rules from: "
+ projectPath.resolve(ExclusionConfigLoader.DEFAULT_FILENAME));
}
}
if (exclusionConfig != null) {
logger.info("Exclusion rules loaded: " + exclusionConfig.getRules().size()
+ " rule(s), affectsExitStatus=" + exclusionConfig.isAffectsExitStatus());
}
} catch (IOException e) {
logger.error("ERROR: Failed to load exclusion config: " + e.getMessage(), e);
return 1;
}

try {
// Execute linting
LinterExecutor executor = new LinterExecutor(
Expand All @@ -185,6 +218,7 @@ public Integer call() {
generateHtmlReport,
generateJsonReport,
!noFailOnErrors,
exclusionConfig,
logger
);

Expand Down
2 changes: 1 addition & 1 deletion linter-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.4.RELEASE</version>
<version>3.1.5.RELEASE</version>
</dependency>

<dependency>
Expand Down
63 changes: 40 additions & 23 deletions linter-core/src/main/java/dev/dsf/linter/DsfLinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import dev.dsf.linter.analysis.LeftoverResourceDetector;
import dev.dsf.linter.exception.MissingServiceRegistrationException;
import dev.dsf.linter.exception.ResourceLinterException;
import dev.dsf.linter.exclusion.ExclusionConfig;
import dev.dsf.linter.exclusion.ExclusionFilter;
import dev.dsf.linter.logger.Console;
import dev.dsf.linter.logger.Logger;
import dev.dsf.linter.report.LintingReportGenerator;
Expand Down Expand Up @@ -51,37 +53,49 @@ public class DsfLinter {
/**
* Configuration for the DSF Linter.
*
* @param projectPath the path to the project root directory
* @param reportPath the path where linting reports should be generated
* @param projectPath the path to the project root directory
* @param reportPath the path where linting reports should be generated
* @param generateHtmlReport whether to generate an HTML report
* @param generateJsonReport whether to generate a JSON report
* @param failOnErrors whether the linter should fail (exit code 1) when errors are found
* @param logger the logger instance for output
* @param failOnErrors whether the linter should fail (exit code 1) when errors are found
* @param exclusionConfig optional exclusion configuration; {@code null} means no exclusions
* @param logger the logger instance for output
*/
public record Config(
Path projectPath,
Path reportPath,
boolean generateHtmlReport,
boolean generateJsonReport,
boolean failOnErrors,
ExclusionConfig exclusionConfig,
Logger logger
) {
/**
* Backward-compatible constructor without exclusion config.
*/
public Config(Path projectPath, Path reportPath, boolean generateHtmlReport,
boolean generateJsonReport, boolean failOnErrors, Logger logger) {
this(projectPath, reportPath, generateHtmlReport, generateJsonReport,
failOnErrors, null, logger);
}
}

/**
* Linting result for a single plugin.
*
* @param pluginName the name of the plugin
* @param pluginClass the fully qualified class name of the plugin
* @param apiVersion the DSF API version used by the plugin
* @param output the detailed linting output (errors, warnings, etc.)
* @param reportPath the path to the generated report for this plugin
* @param pluginName the name of the plugin
* @param pluginClass the fully qualified class name of the plugin
* @param apiVersion the DSF API version used by the plugin
* @param output the linting output after exclusions have been applied (used for reports)
* @param excludedErrorCount the number of ERROR-severity items that were excluded by exclusion rules
* @param reportPath the path to the generated report for this plugin
*/
public record PluginLinter(
String pluginName,
String pluginClass,
ApiVersion apiVersion,
LintingOutput output,
int excludedErrorCount,
Path reportPath
) {
}
Expand Down Expand Up @@ -128,12 +142,6 @@ public int getLeftoverCount() {
return leftoverAnalysis != null ? leftoverAnalysis.getTotalLeftoverCount() : 0;
}

/**
* Get total error count across all plugins and project-level leftovers.
*/
public int getTotalErrors() {
return getPluginErrors() + getLeftoverCount();
}
}

private final Config config;
Expand Down Expand Up @@ -165,6 +173,9 @@ public DsfLinter(Config config) {
this.config = config;
this.logger = config.logger();
Console.init(logger);
ExclusionFilter exclusionFilter = config.exclusionConfig() != null
? new ExclusionFilter(config.exclusionConfig())
: null;
this.setupHandler = new ProjectSetupHandler(logger);
this.discoveryService = new ResourceDiscoveryService(logger);
BpmnLintingService bpmnLinter = new BpmnLintingService(logger);
Expand All @@ -179,6 +190,7 @@ public DsfLinter(Config config) {
leftoverDetector,
reportGenerator,
config.reportPath(),
exclusionFilter,
logger
);
}
Expand Down Expand Up @@ -211,11 +223,7 @@ public OverallLinterResult lint() throws IOException {
// Phase 1: Project Setup
reportGenerator.printPhaseHeader("Phase 1: Project Setup");
ProjectSetupHandler.ProjectContext context;
try {
context = setupHandler.setupLintingEnvironment(config.projectPath());
} catch (IOException e) {
throw e;
}
context = setupHandler.setupLintingEnvironment(config.projectPath());

// Execute all linting phases with temporary context classloader
return ClassLoaderUtils.withTemporaryContextClassLoader(context.projectClassLoader(), () -> {
Expand Down Expand Up @@ -253,11 +261,20 @@ public OverallLinterResult lint() throws IOException {
long executionTime = System.currentTimeMillis() - startTime;
reportGenerator.printSummary(pluginLinting, discovery, leftoverResults, executionTime, config);

// Determine final success status
// Determine final success status.
// v.output().getErrorCount() reflects only included (non-excluded) items.
// When affectsExitStatus=true, add back the count of excluded errors.
boolean addExcludedToCount = config.exclusionConfig() != null
&& config.exclusionConfig().isAffectsExitStatus();

int totalPluginErrors = pluginLinting.values().stream()
.mapToInt(v -> v.output().getErrorCount())
.mapToInt(v -> {
int reported = v.output().getErrorCount();
int excluded = addExcludedToCount ? v.excludedErrorCount() : 0;
return reported + excluded;
})
.sum();

// Consider failed plugins as errors (partial success means non-zero exit code)
boolean hasFailedPlugins = discovery.hasFailedPlugins();
boolean success = !config.failOnErrors() || (totalPluginErrors == 0 && !hasFailedPlugins);
Expand Down
Loading