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