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
4 changes: 4 additions & 0 deletions plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (

## [Unreleased]

### Changes
- Add support to apply alternate license header within same format ([#872](https://github.com/diffplug/spotless/issues/872))
- Add support to skip license header application based on source file content pattern ([#650](https://github.com/diffplug/spotless/issues/650)).

## [3.9.0] - 2026-07-27
### Added
- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space) with the new `<princeOfSpace>` step. ([#2991](https://github.com/diffplug/spotless/pull/2991))
Expand Down
19 changes: 19 additions & 0 deletions plugin-maven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,25 @@ Some files have fixed header lines (e.g. `<?xml version="1.0" ...` in XMLs, or `

To define what lines to skip at the beginning of such files, fill the `skipLinesMatching` option with a regular expression that matches them (e.g. `<skipLinesMatching>^#!.+?$</skipLinesMatching>` to skip shebangs).

### Skip headers and multiple license headers

Sometimes it may be necessary to disable a license rule for specific files, or to maintain dual licenses.

To define alternate replacements:

```xml
<licenseHeader>
<name>PrimaryHeaderLicense</name>
<content>/** Base License Header */</content>
<onlyIfContentMatches>Best</onlyIfContentMatches>
</licenseHeader>
<licenseHeader>
<name>SecondaryHeaderLicense</name>
<content>/** Alternate License Header */</content>
<onlyIfContentMatches>.*Test.+</onlyIfContentMatches>
</licenseHeader>
```

<a name="invisible"></a>

<a name="ratchet"></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@

public class LicenseHeader implements FormatterStepFactory {

@Parameter
private String name;

@Parameter
private String onlyIfContentMatches;

@Parameter
private String file;

Expand Down Expand Up @@ -57,12 +63,17 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) {
boolean updateYear = config.getRatchetFrom().isPresent();
yearMode = updateYear ? YearMode.UPDATE_TO_TODAY : YearMode.PRESERVE;
}
return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString)
.withYearMode(yearMode)
.withSkipLinesMatching(skipLinesMatching)
.withYearStingFormat(yearStrFmt)
.build()
.filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter());
LicenseHeaderStep builder = LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString)
.withYearMode(yearMode)
.withSkipLinesMatching(skipLinesMatching)
.withYearStingFormat(yearStrFmt);
if (name != null) {
builder = builder.withName(name);
}
if (onlyIfContentMatches != null) {
builder = builder.withContentPattern(onlyIfContentMatches);
}
return builder.build().filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter());
} else {
throw new IllegalArgumentException("Must specify exactly one of 'file' or 'content'.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
class LicenseHeaderTest extends MavenIntegrationHarness {
private static final String KEY_LICENSE = "license/TestLicense";
private static final String KOTLIN_LICENSE_HEADER = "// Hello, I'm Kotlin license header";
private static final String TEST_JAVA = "src/main/java/pkg/Test.java";
private static final String CONTENT = "package pkg;\npublic class Test {}";

@Test
void fromFileJava() throws Exception {
Expand All @@ -33,6 +35,64 @@ void fromFileJava() throws Exception {
runTest();
}

/**
* When {@code onlyIfContentMatches} matches the file content, the license header is applied.
*/
@Test
void onlyIfContentMatchesAppliesWhenPatternMatches() throws Exception {
writePomWithJavaSteps(
"<licenseHeader>",
" <content>/** New License Header */</content>",
" <onlyIfContentMatches>.+Test.+</onlyIfContentMatches>",
"</licenseHeader>");

setFile(TEST_JAVA).toContent(CONTENT);
mavenRunner().withArguments("spotless:apply").runNoError();
assertFile(TEST_JAVA).hasContent("/** New License Header */\n" + CONTENT);
}

/**
* When {@code onlyIfContentMatches} does not match the file content, the existing
* content (including any existing header) is left unchanged.
*/
@Test
void onlyIfContentMatchesSkipsWhenPatternDoesNotMatch() throws Exception {
writePomWithJavaSteps(
"<licenseHeader>",
" <content>/** Should Not Be Applied */</content>",
" <onlyIfContentMatches>missingString</onlyIfContentMatches>",
"</licenseHeader>");

String existing = "/** This license header should be preserved */\n" + CONTENT;
setFile(TEST_JAVA).toContent(existing);
mavenRunner().withArguments("spotless:apply").runNoError();
assertFile(TEST_JAVA).hasContent(existing);
}

/**
* Multiple named license-header steps with different {@code onlyIfContentMatches}
* patterns: the step whose pattern matches is applied (parity with Gradle
* {@code filterByContentPatternTest}).
*/
@Test
void multipleNamedLicenseHeadersSelectByContentPattern() throws Exception {
writePomWithJavaSteps(
"<licenseHeader>",
" <name>PrimaryHeaderLicense</name>",
" <content>/** Base License Header */</content>",
" <onlyIfContentMatches>Best</onlyIfContentMatches>",
"</licenseHeader>",
"<licenseHeader>",
" <name>SecondaryHeaderLicense</name>",
" <content>/** Alternate License Header */</content>",
" <onlyIfContentMatches>.*Test.+</onlyIfContentMatches>",
"</licenseHeader>");

setFile(TEST_JAVA).toContent("/** 2003 */\n" + CONTENT);
mavenRunner().withArguments("spotless:apply").runNoError();
assertFile(TEST_JAVA).hasContent("/** Alternate License Header */\n" + CONTENT);
}

@Test
void fromContentCpp() throws Exception {
String cppLicense = "//my license";
Expand Down