diff --git a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
index eec6274c8d..817bb3bc1c 100644
--- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java
@@ -20,6 +20,7 @@
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.Files;
+import java.time.YearMonth;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -35,6 +36,11 @@ public final class LicenseHeaderStep implements Serializable {
private final String licenseHeader;
private final Pattern delimiterPattern;
+ private Pattern yearMatcherPattern;
+ private boolean hasYearToken;
+ private String licenseHeaderBeforeYearToken;
+ private String licenseHeaderAfterYearToken;
+ private String licenseHeaderWithYearTokenReplaced;
/** Creates a FormatterStep which forces the start of each file to match a license header. */
public static FormatterStep createFromHeader(String licenseHeader, String delimiter) {
@@ -74,6 +80,14 @@ private LicenseHeaderStep(String licenseHeader, String delimiter) {
}
this.licenseHeader = licenseHeader;
this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE);
+ this.hasYearToken = licenseHeader.contains("$YEAR");
+ if (this.hasYearToken) {
+ int yearTokenIndex = licenseHeader.indexOf("$YEAR");
+ this.licenseHeaderBeforeYearToken = licenseHeader.substring(0, yearTokenIndex);
+ this.licenseHeaderAfterYearToken = licenseHeader.substring(yearTokenIndex + 5, licenseHeader.length());
+ this.licenseHeaderWithYearTokenReplaced = licenseHeader.replace("$YEAR", String.valueOf(YearMonth.now().getYear()));
+ this.yearMatcherPattern = Pattern.compile("[0-9]{4}(-[0-9]{4})?");
+ }
}
/** Reads the license file from the given file. */
@@ -87,7 +101,14 @@ public String format(String raw) {
if (!matcher.find()) {
throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern);
} else {
- if (matcher.start() == licenseHeader.length() && raw.startsWith(licenseHeader)) {
+ if (hasYearToken) {
+ if (matchesLicenseWithYearToken(raw, matcher)) {
+ // that means we have the license like `licenseHeaderBeforeYearToken 1990-2015 licenseHeaderAfterYearToken`
+ return raw;
+ } else {
+ return licenseHeaderWithYearTokenReplaced + raw.substring(matcher.start());
+ }
+ } else if (matcher.start() == licenseHeader.length() && raw.startsWith(licenseHeader)) {
// if no change is required, return the raw string without
// creating any other new strings for maximum performance
return raw;
@@ -97,4 +118,10 @@ public String format(String raw) {
}
}
}
+
+ private boolean matchesLicenseWithYearToken(String raw, Matcher matcher) {
+ int startOfTheSecondPart = raw.indexOf(licenseHeaderAfterYearToken);
+ return (raw.startsWith(licenseHeaderBeforeYearToken) && startOfTheSecondPart + licenseHeaderAfterYearToken.length() == matcher.start())
+ && yearMatcherPattern.matcher(raw.substring(licenseHeaderBeforeYearToken.length(), startOfTheSecondPart)).matches();
+ }
}
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index c921e86228..6efaf92ac5 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -273,6 +273,37 @@ spotless {
}
```
+
+
+## License header options
+
+If the string contents of a licenseHeader step or the file contents of a licenseHeaderFile step contains a $YEAR token,
+then in the end-result generated license headers which use this license header as a template, $YEAR will be replaced with the current year.
+
+
+For example:
+```
+/* Licensed under Apache-2.0 $YEAR. */
+```
+will produce
+```
+/* Licensed under Apache-2.0 2017. */
+```
+if Spotless is launched in 2017
+
+
+The `licenseHeader` and `licenseHeaderFile` steps will generate license headers with automatic years from the base license header according to the following rules:
+* A generated license header will be updated with the current year when
+ * the generated license header is missing
+ * the generated license header is not formatted correctly
+* A generated license header will _not_ be updated when
+ * a single year is already present, e.g.
+ `/* Licensed under Apache-2.0 1990. */`
+ * a hyphen-separated year range is already present, e.g.
+ `/* Licensed under Apache-2.0 1990-2003. */`
+ * the `$YEAR` token is otherwise missing
+
+
## Custom rules
diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
index 5be629bc0f..2f57af6212 100644
--- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java
@@ -16,7 +16,9 @@
package com.diffplug.spotless.generic;
import java.io.File;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.time.YearMonth;
import org.junit.Assert;
import org.junit.Test;
@@ -24,12 +26,18 @@
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.ResourceHarness;
import com.diffplug.spotless.SerializableEqualityTester;
+import com.diffplug.spotless.StepHarness;
public class LicenseHeaderStepTest extends ResourceHarness {
private static final String KEY_LICENSE = "license/TestLicense";
private static final String KEY_FILE_NOTAPPLIED = "license/MissingLicense.test";
private static final String KEY_FILE_APPLIED = "license/HasLicense.test";
+ // files to test $YEAR token replacement
+ private static final String KEY_LICENSE_WITH_YEAR_TOKEN = "license/LicenseHeaderWithYearToken";
+ private static final String KEY_FILE_WITHOUT_LICENSE = "license/FileWithoutLicenseHeader.test";
+ private static final String KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER = "license/FileWithLicenseHeaderAndPlaceholder.test";
+
// If this constant changes, don't forget to change the similarly-named one in
// plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java as well
private static final String LICENSE_HEADER_DELIMITER = "package ";
@@ -46,6 +54,26 @@ public void fromFile() throws Throwable {
assertOnResources(step, KEY_FILE_NOTAPPLIED, KEY_FILE_APPLIED);
}
+ @Test
+ public void should_apply_license_containing_YEAR_token() throws Throwable {
+ FormatterStep step = LicenseHeaderStep.createFromFile(createTestFile(KEY_LICENSE_WITH_YEAR_TOKEN), StandardCharsets.UTF_8, LICENSE_HEADER_DELIMITER);
+
+ StepHarness.forStep(step)
+ .test(getTestResource(KEY_FILE_WITHOUT_LICENSE), fileWithPlaceholderContaining(currentYear()))
+ .testUnaffected(fileWithPlaceholderContaining(currentYear()))
+ .testUnaffected(fileWithPlaceholderContaining("2003"))
+ .testUnaffected(fileWithPlaceholderContaining("1990-2015"))
+ .test(fileWithPlaceholderContaining("not a year"), fileWithPlaceholderContaining(currentYear()));
+ }
+
+ private String fileWithPlaceholderContaining(String placeHolderContent) throws IOException {
+ return getTestResource(KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER).replace("__PLACEHOLDER__", placeHolderContent);
+ }
+
+ private String currentYear() {
+ return String.valueOf(YearMonth.now().getYear());
+ }
+
@Test
public void efficient() throws Throwable {
FormatterStep step = LicenseHeaderStep.createFromHeader("LicenseHeader\n", "contentstart");
diff --git a/testlib/src/test/resources/license/FileWithLicenseHeaderAndPlaceholder.test b/testlib/src/test/resources/license/FileWithLicenseHeaderAndPlaceholder.test
new file mode 100644
index 0000000000..68c5b3a8cc
--- /dev/null
+++ b/testlib/src/test/resources/license/FileWithLicenseHeaderAndPlaceholder.test
@@ -0,0 +1,31 @@
+/*
+ * This is a fake license, __PLACEHOLDER__. ACME corp.
+ **/
+package com.acme;
+
+import java.util.function.Function;
+
+
+public class Java8Test {
+ public void doStuff() throws Exception {
+ Function example = Integer::parseInt;
+ example.andThen(val -> {
+ return val + 2;
+ } );
+ SimpleEnum val = SimpleEnum.A;
+ switch (val) {
+ case A:
+ break;
+ case B:
+ break;
+ case C:
+ break;
+ default:
+ throw new Exception();
+ }
+ }
+
+ public enum SimpleEnum {
+ A, B, C;
+ }
+}
diff --git a/testlib/src/test/resources/license/FileWithoutLicenseHeader.test b/testlib/src/test/resources/license/FileWithoutLicenseHeader.test
new file mode 100644
index 0000000000..297c16a214
--- /dev/null
+++ b/testlib/src/test/resources/license/FileWithoutLicenseHeader.test
@@ -0,0 +1,28 @@
+package com.acme;
+
+import java.util.function.Function;
+
+
+public class Java8Test {
+ public void doStuff() throws Exception {
+ Function example = Integer::parseInt;
+ example.andThen(val -> {
+ return val + 2;
+ } );
+ SimpleEnum val = SimpleEnum.A;
+ switch (val) {
+ case A:
+ break;
+ case B:
+ break;
+ case C:
+ break;
+ default:
+ throw new Exception();
+ }
+ }
+
+ public enum SimpleEnum {
+ A, B, C;
+ }
+}
diff --git a/testlib/src/test/resources/license/LicenseHeaderWithYearToken b/testlib/src/test/resources/license/LicenseHeaderWithYearToken
new file mode 100644
index 0000000000..aa3cac89df
--- /dev/null
+++ b/testlib/src/test/resources/license/LicenseHeaderWithYearToken
@@ -0,0 +1,3 @@
+/*
+ * This is a fake license, $YEAR. ACME corp.
+ **/
\ No newline at end of file