diff --git a/CHANGES.md b/CHANGES.md index 89bc6a1231..bc19d79cf5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Changed +* When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). ## [2.5.0] - 2020-09-08 ### Added 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 3066113d3b..40eee8a6f4 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java @@ -181,8 +181,6 @@ private Runtime(String licenseHeader, String delimiter, String yearSeparator, bo } } - private static final Pattern patternYearSingle = Pattern.compile("[0-9]{4}"); - /** * Get the first place holder token being used in the * license header for specifying the year @@ -200,6 +198,7 @@ private String format(String raw) { if (!contentMatcher.find()) { throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern); } else { + String content = raw.substring(contentMatcher.start()); if (yearToday == null) { // the no year case is easy if (contentMatcher.start() == yearSepOrFull.length() && raw.startsWith(yearSepOrFull)) { @@ -208,7 +207,7 @@ private String format(String raw) { return raw; } else { // otherwise we'll have to add the header - return yearSepOrFull + raw.substring(contentMatcher.start()); + return yearSepOrFull + content; } } else { // the yes year case is a bit harder @@ -216,47 +215,65 @@ private String format(String raw) { int afterYearIdx = raw.indexOf(afterYear, beforeYearIdx + beforeYear.length() + 1); if (beforeYearIdx >= 0 && afterYearIdx >= 0 && afterYearIdx + afterYear.length() <= contentMatcher.start()) { - boolean noPadding = beforeYearIdx == 0 && afterYearIdx + afterYear.length() == contentMatcher.start(); // allows fastpath return raw - String parsedYear = raw.substring(beforeYearIdx + beforeYear.length(), afterYearIdx); - if (parsedYear.equals(yearToday)) { - // it's good as is! - return noPadding ? raw : beforeYear + yearToday + afterYear + raw.substring(contentMatcher.start()); - } else if (patternYearSingle.matcher(parsedYear).matches()) { - if (updateYearWithLatest) { - // expand from `2004` to `2004-2020` - return beforeYear + parsedYear + yearSepOrFull + yearToday + afterYear + raw.substring(contentMatcher.start()); - } else { - // it's already good as a single year - return noPadding ? raw : beforeYear + parsedYear + afterYear + raw.substring(contentMatcher.start()); - } - } else { - Matcher yearMatcher = patternYearSingle.matcher(parsedYear); - if (yearMatcher.find()) { - String firstYear = yearMatcher.group(); - String newYear; - String secondYear; - if (updateYearWithLatest) { - secondYear = firstYear.equals(yearToday) ? null : yearToday; - } else if (yearMatcher.find(yearMatcher.end() + 1)) { - secondYear = yearMatcher.group(); - } else { - secondYear = null; - } - if (secondYear == null) { - newYear = firstYear; - } else { - newYear = firstYear + yearSepOrFull + secondYear; - } - return noPadding && newYear.equals(parsedYear) ? raw : beforeYear + newYear + afterYear + raw.substring(contentMatcher.start()); + // and also ends with exactly the right header, so it's easy to parse the existing year + String existingYear = raw.substring(beforeYearIdx + beforeYear.length(), afterYearIdx); + String newYear = calculateYearExact(existingYear); + if (existingYear.equals(newYear)) { + // fastpath where we don't need to make any changes at all + boolean noPadding = beforeYearIdx == 0 && afterYearIdx + afterYear.length() == contentMatcher.start(); // allows fastpath return raw + if (noPadding) { + return raw; } } + return beforeYear + newYear + afterYear + content; + } else { + String newYear = calculateYearBySearching(raw.substring(0, contentMatcher.start())); + // at worst, we just say that it was made today + return beforeYear + newYear + afterYear + content; } - // at worst, we just say that it was made today - return beforeYear + yearToday + afterYear + raw.substring(contentMatcher.start()); } } } + private static final Pattern YYYY = Pattern.compile("[0-9]{4}"); + + /** Calculates the year to inject. */ + private String calculateYearExact(String parsedYear) { + if (parsedYear.equals(yearToday)) { + return parsedYear; + } else if (YYYY.matcher(parsedYear).matches()) { + if (updateYearWithLatest) { + return parsedYear + yearSepOrFull + yearToday; + } else { + // it's already good as a single year + return parsedYear; + } + } else { + return calculateYearBySearching(parsedYear); + } + } + + /** Searches the given string for YYYY, and uses that to determine the year range. */ + private String calculateYearBySearching(String content) { + Matcher yearMatcher = YYYY.matcher(content); + if (yearMatcher.find()) { + String firstYear = yearMatcher.group(); + String secondYear; + if (updateYearWithLatest) { + secondYear = firstYear.equals(yearToday) ? null : yearToday; + } else if (yearMatcher.find(yearMatcher.end() + 1)) { + secondYear = yearMatcher.group(); + } else { + secondYear = null; + } + return secondYear == null ? firstYear : firstYear + yearSepOrFull + secondYear; + } else { + System.err.println("Can't parse copyright year '" + content + "', defaulting to " + yearToday); + // couldn't recognize the year format + return yearToday; + } + } + /** Sets copyright years on the given file by finding the oldest and most recent commits throughout git history. */ private String setLicenseHeaderYearsFromGitHistory(String raw, File file) throws IOException { if (yearToday == null) { diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index e942ae1218..7d8f339840 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Changed +* When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). ## [5.4.0] - 2020-09-08 ### Added diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 951e0ea9a2..f435bb8911 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Changed +* When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). ## [2.2.0] - 2020-09-08 ### Added diff --git a/testlib/src/main/resources/license/FileWithLicenseHeaderAndPlaceholder.test b/testlib/src/main/resources/license/FileWithLicenseHeaderAndPlaceholder.test deleted file mode 100644 index 3b43b9be2c..0000000000 --- a/testlib/src/main/resources/license/FileWithLicenseHeaderAndPlaceholder.test +++ /dev/null @@ -1,31 +0,0 @@ -/* - * __LICENSE_PLACEHOLDER__ - **/ -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/main/resources/license/LicenseHeaderWithPlaceholder b/testlib/src/main/resources/license/LicenseHeaderWithPlaceholder deleted file mode 100644 index fb22c11899..0000000000 --- a/testlib/src/main/resources/license/LicenseHeaderWithPlaceholder +++ /dev/null @@ -1,3 +0,0 @@ -/* - * __LICENSE_PLACEHOLDER__ - **/ \ No newline at end of file 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 9ca2f12245..85f44d8265 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -29,108 +29,108 @@ import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode; 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_FILE_WITHOUT_LICENSE = "license/FileWithoutLicenseHeader.test"; - // Templates to test with custom license contents - private static final String KEY_LICENSE_WITH_PLACEHOLDER = "license/LicenseHeaderWithPlaceholder"; - private static final String KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER = "license/FileWithLicenseHeaderAndPlaceholder.test"; - // Licenses to test $YEAR token replacement - private static final String HEADER_WITH_YEAR = "This is a fake license, $YEAR. ACME corp."; - // License to test $today.year token replacement - private static final String HEADER_WITH_YEAR_INTELLIJ = "This is a fake license, $today.year. ACME corp."; - // Special case where the characters immediately before and after the year token are the same, - // start position of the second part might overlap the end position of the first part. - private static final String HEADER_WITH_YEAR_VARIANT = "This is a fake license. Copyright $YEAR ACME corp."; - - // 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 "; + private static final String FILE_NO_LICENSE = "license/FileWithoutLicenseHeader.test"; + private static final String package_ = "package "; + private static final String HEADER_WITH_$YEAR = "This is a fake license, $YEAR. ACME corp."; + + @Test + public void parseExistingYear() throws Exception { + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_).build()) + // has existing + .test(hasHeader("This is a fake license, 2007. ACME corp."), hasHeader("This is a fake license, 2007. ACME corp.")) + // if prefix changes, the year will get set to today + .test(hasHeader("This is a license, 2007. ACME corp."), hasHeader("This is a fake license, 2007. ACME corp.")) + // if suffix changes, the year will get set to today + .test(hasHeader("This is a fake license, 2007. Other corp."), hasHeader("This is a fake license, 2007. ACME corp.")); + } @Test public void fromHeader() throws Throwable { - FormatterStep step = LicenseHeaderStep.headerDelimiter(getTestResource(KEY_LICENSE), LICENSE_HEADER_DELIMITER).build(); - assertOnResources(step, KEY_FILE_NOTAPPLIED, KEY_FILE_APPLIED); + FormatterStep step = LicenseHeaderStep.headerDelimiter(getTestResource("license/TestLicense"), package_).build(); + StepHarness.forStep(step) + .testResource("license/MissingLicense.test", "license/HasLicense.test"); } @Test public void should_apply_license_containing_YEAR_token() throws Throwable { - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR), LICENSE_HEADER_DELIMITER).build()) - .test(getTestResource(KEY_FILE_WITHOUT_LICENSE), fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "2003")) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "1990-2015")) - .test(fileContainingYear("Something before license.*/\n/* \n * " + HEADER_WITH_YEAR, "2003"), fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .test(fileContainingYear(HEADER_WITH_YEAR + "\n **/\n/* Something after license.", "2003"), fileContainingYear(HEADER_WITH_YEAR, "2003")) - .test(fileContainingYear(HEADER_WITH_YEAR, "not a year"), fileContainingYear(HEADER_WITH_YEAR, currentYear())); + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_).build()) + .test(getTestResource(FILE_NO_LICENSE), hasHeaderYear(currentYear())) + .testUnaffected(hasHeaderYear(currentYear())) + .testUnaffected(hasHeaderYear("2003")) + .testUnaffected(hasHeaderYear("1990-2015")) + .test(hasHeaderYear("Something before license.*/\n/* \n * " + HEADER_WITH_$YEAR, "2003"), hasHeaderYear("2003")) + .test(hasHeaderYear(HEADER_WITH_$YEAR + "\n **/\n/* Something after license.", "2003"), hasHeaderYear("2003")) + .test(hasHeaderYear("not a year"), hasHeaderYear(currentYear())); // Check with variant - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR_VARIANT), LICENSE_HEADER_DELIMITER).build()) - .test(getTestResource(KEY_FILE_WITHOUT_LICENSE), fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())) - .test(fileContaining("This is a fake license. Copyright "), fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())) - .test(fileContaining(" ACME corp."), fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())) - .test(fileContaining("This is a fake license. Copyright ACME corp."), fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())) - .test(fileContaining("This is a fake license. CopyrightACME corp."), fileContainingYear(HEADER_WITH_YEAR_VARIANT, currentYear())); + String otherFakeLicense = "This is a fake license. Copyright $YEAR ACME corp."; + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(otherFakeLicense), package_).build()) + .test(getTestResource(FILE_NO_LICENSE), hasHeaderYear(otherFakeLicense, currentYear())) + .testUnaffected(hasHeaderYear(otherFakeLicense, currentYear())) + .test(hasHeader("This is a fake license. Copyright "), hasHeaderYear(otherFakeLicense, currentYear())) + .test(hasHeader(" ACME corp."), hasHeaderYear(otherFakeLicense, currentYear())) + .test(hasHeader("This is a fake license. Copyright ACME corp."), hasHeaderYear(otherFakeLicense, currentYear())) + .test(hasHeader("This is a fake license. CopyrightACME corp."), hasHeaderYear(otherFakeLicense, currentYear())); //Check when token is of the format $today.year - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR_INTELLIJ), LICENSE_HEADER_DELIMITER).build()) - .test(fileContaining(HEADER_WITH_YEAR_INTELLIJ), fileWithLicenseContaining(HEADER_WITH_YEAR_INTELLIJ, currentYear(), "$today.year")); - } - - private String fileWithLicenseContaining(String license, String yearContent, String token) throws IOException { - return getTestResource(KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER).replace("__LICENSE_PLACEHOLDER__", license).replace(token, yearContent); + String HEADER_WITH_YEAR_INTELLIJ = "This is a fake license, $today.year. ACME corp."; + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_YEAR_INTELLIJ), package_).build()) + .test(hasHeader(HEADER_WITH_YEAR_INTELLIJ), hasHeader(HEADER_WITH_YEAR_INTELLIJ.replace("$today.year", currentYear()))); } @Test public void updateYearWithLatest() throws Throwable { - FormatterStep step = LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR), LICENSE_HEADER_DELIMITER) + FormatterStep step = LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_) .withYearMode(YearMode.UPDATE_TO_TODAY) .build(); StepHarness.forStep(step) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .test(fileContainingYear(HEADER_WITH_YEAR, "2003"), fileContainingYear(HEADER_WITH_YEAR, "2003-" + currentYear())) - .test(fileContainingYear(HEADER_WITH_YEAR, "1990-2015"), fileContainingYear(HEADER_WITH_YEAR, "1990-" + currentYear())); + .testUnaffected(hasHeaderYear(currentYear())) + .test(hasHeaderYear("2003"), hasHeaderYear("2003-" + currentYear())) + .test(hasHeaderYear("1990-2015"), hasHeaderYear("1990-" + currentYear())); } @Test public void should_apply_license_containing_YEAR_token_with_non_default_year_separator() throws Throwable { - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR), LICENSE_HEADER_DELIMITER).withYearSeparator(", ").build()) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "1990, 2015")) - .test(fileContainingYear(HEADER_WITH_YEAR, "1990-2015"), fileContainingYear(HEADER_WITH_YEAR, "1990, 2015")); + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_).withYearSeparator(", ").build()) + .testUnaffected(hasHeaderYear("1990, 2015")) + .test(hasHeaderYear("1990-2015"), hasHeaderYear("1990, 2015")); } @Test public void should_apply_license_containing_YEAR_token_with_special_character_in_year_separator() throws Throwable { - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR), LICENSE_HEADER_DELIMITER).withYearSeparator("(").build()) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "1990(2015")) - .test(fileContainingYear(HEADER_WITH_YEAR, "1990-2015"), fileContainingYear(HEADER_WITH_YEAR, "1990(2015")); + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_).withYearSeparator("(").build()) + .testUnaffected(hasHeaderYear("1990(2015")) + .test(hasHeaderYear("1990-2015"), hasHeaderYear("1990(2015")); } @Test public void should_apply_license_containing_YEAR_token_with_custom_separator() throws Throwable { - StepHarness.forStep(LicenseHeaderStep.headerDelimiter(licenseWith(HEADER_WITH_YEAR), LICENSE_HEADER_DELIMITER).build()) - .test(getTestResource(KEY_FILE_WITHOUT_LICENSE), fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, currentYear())) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "2003")) - .testUnaffected(fileContainingYear(HEADER_WITH_YEAR, "1990-2015")) - .test(fileContainingYear(HEADER_WITH_YEAR, "not a year"), fileContainingYear(HEADER_WITH_YEAR, currentYear())); + StepHarness.forStep(LicenseHeaderStep.headerDelimiter(header(HEADER_WITH_$YEAR), package_).build()) + .test(getTestResource(FILE_NO_LICENSE), hasHeaderYear(currentYear())) + .testUnaffected(hasHeaderYear(currentYear())) + .testUnaffected(hasHeaderYear("2003")) + .testUnaffected(hasHeaderYear("1990-2015")) + .test(hasHeaderYear("not a year"), hasHeaderYear(currentYear())); + } + + private String header(String contents) throws IOException { + return "/*\n" + + " * " + contents + "\n" + + " **/\n"; } - private String licenseWith(String contents) throws IOException { - return getTestResource(KEY_LICENSE_WITH_PLACEHOLDER).replace("__LICENSE_PLACEHOLDER__", contents); + private String hasHeader(String header) throws IOException { + return header(header) + getTestResource(FILE_NO_LICENSE); } - private String fileContaining(String license) throws IOException { - return fileContainingYear(license, ""); + private String hasHeaderYear(String license, String years) throws IOException { + return header(license).replace("$YEAR", years) + getTestResource(FILE_NO_LICENSE); } - private String fileContainingYear(String license, String yearContent) throws IOException { - return getTestResource(KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER).replace("__LICENSE_PLACEHOLDER__", license).replace("$YEAR", yearContent); + private String hasHeaderYear(String years) throws IOException { + return hasHeaderYear(HEADER_WITH_$YEAR, years); } - private String currentYear() { + private static String currentYear() { return String.valueOf(YearMonth.now().getYear()); }