Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LicenseHeaderStep more permissive with parsing existing years #690

Merged
merged 5 commits into from
Sep 10, 2020
Merged
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)) {
Expand All @@ -208,55 +207,73 @@ 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
int beforeYearIdx = raw.indexOf(beforeYear);
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) {
Expand Down
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down