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..e78e9dec6f 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); + hasYearToken = licenseHeader.contains("$YEAR"); + if (hasYearToken) { + int yearTokenIndex = licenseHeader.indexOf("$YEAR"); + licenseHeaderBeforeYEARToken = licenseHeader.substring(0, yearTokenIndex); + licenseHeaderAfterYEARToken = licenseHeader.substring(yearTokenIndex + 5, licenseHeader.length()); + 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..c3c1a5b4b2 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -273,6 +273,32 @@ spotless { } ``` + + +## License header options + +The license header can contains a `$YEAR` variable that will be replaced by the current year. + +For example: +``` +/* Licensed under Apache-2.0 $YEAR. */ +``` +will produce +``` +/* Licensed under Apache-2.0 2017. */ +``` +if build is launched in 2017 + + +The step will change the license according to the following rules +* It replace the license using the current year when + * The license is missing + * The license is not formatted correctly +* It will *not* replace the license when + * The year variable is already present and is a single year, e.g. `/* Licensed under Apache-2.0 1990. */` + * The year variable is already present and is a year span, e.g. `/* Licensed under Apache-2.0 1990-2003. */` + + ## 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..61c42f6bdc 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -16,8 +16,11 @@ package com.diffplug.spotless.generic; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.YearMonth; +import com.diffplug.spotless.StepHarness; import org.junit.Assert; import org.junit.Test; @@ -29,6 +32,11 @@ 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"; + private static final String KEY_LICENSE_WITH_YEAR = "license/TestLicencseWithYear"; + private static final String KEY_FILE_WITHOUT_LICENSE = "license/MissLicenseWithYear.test"; + private static final String KEY_FILE_WITH_PREVIOUS_YEAR = "license/LicenseWithPreviousYear.test"; + private static final String KEY_FILE_WITH_PREVIOUS_YEARS = "license/LicenseWithPreviousYears.test"; + private static final String KEY_FILE_WITH_CURRENT_YEAR = "license/LicenseWithYear.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 @@ -46,6 +54,27 @@ public void fromFile() throws Throwable { assertOnResources(step, KEY_FILE_NOTAPPLIED, KEY_FILE_APPLIED); } + @Test + public void should_apply_license_containing_YEAR_variable() throws Throwable { + FormatterStep step = LicenseHeaderStep.createFromFile(createTestFile(KEY_LICENSE_WITH_YEAR), StandardCharsets.UTF_8, LICENSE_HEADER_DELIMITER); + + + StepHarness.forStep(step) + .test(getTestResource(KEY_FILE_WITHOUT_LICENSE), getFileContentWithYEAR(currentYear())) + .testUnaffected(getFileContentWithYEAR(currentYear())) + .testUnaffected(getFileContentWithYEAR("2003")) + .testUnaffected(getFileContentWithYEAR("1990-2015")) + .test(getFileContentWithYEAR("not a year"), getFileContentWithYEAR(currentYear())); + } + + private String getFileContentWithYEAR(String year) throws IOException { + return getTestResource(KEY_FILE_WITH_CURRENT_YEAR).replace("__YEAR_to_replace_in_tests__", year); + } + + 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/LicenseWithYear.test b/testlib/src/test/resources/license/LicenseWithYear.test new file mode 100644 index 0000000000..6a4905bf08 --- /dev/null +++ b/testlib/src/test/resources/license/LicenseWithYear.test @@ -0,0 +1,40 @@ +/* + * Copyright (C) __YEAR_to_replace_in_tests__. ACME corp. + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +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/MissLicenseWithYear.test b/testlib/src/test/resources/license/MissLicenseWithYear.test new file mode 100644 index 0000000000..297c16a214 --- /dev/null +++ b/testlib/src/test/resources/license/MissLicenseWithYear.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/TestLicencseWithYear b/testlib/src/test/resources/license/TestLicencseWithYear new file mode 100644 index 0000000000..39e6b98a52 --- /dev/null +++ b/testlib/src/test/resources/license/TestLicencseWithYear @@ -0,0 +1,12 @@ +/* + * Copyright (C) $YEAR. ACME corp. + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ \ No newline at end of file