Skip to content

Commit bfb89e4

Browse files
authored
Merge pull request #593 from diffplug/feat/license-update
Make it easier to keep copyright headers up-to-date
2 parents 061b981 + e318a14 commit bfb89e4

File tree

9 files changed

+291
-137
lines changed

9 files changed

+291
-137
lines changed

Diff for: CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
* `LicenseHeaderStep` now has an `updateYearWithLatest` parameter which can update copyright headers to today's date. ([#593](https://github.com/diffplug/spotless/pull/593))
15+
* Parsing of existing years from headers is now more lenient.
16+
* The `LicenseHeaderStep` constructor is now public, which allows capturing its state lazily, which is helpful for setting defaults based on `ratchetFrom`.
1317

1418
## [1.32.0] - 2020-06-01
1519
### Added

Diff for: lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java

+79-46
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package com.diffplug.spotless.generic;
1717

1818
import java.io.File;
19-
import java.io.IOException;
2019
import java.io.Serializable;
2120
import java.nio.charset.Charset;
2221
import java.nio.file.Files;
@@ -28,6 +27,8 @@
2827
import java.util.regex.Matcher;
2928
import java.util.regex.Pattern;
3029

30+
import javax.annotation.Nullable;
31+
3132
import com.diffplug.spotless.FormatterStep;
3233
import com.diffplug.spotless.LineEnding;
3334
import com.diffplug.spotless.SerializableFileFilter;
@@ -43,14 +44,6 @@ public final class LicenseHeaderStep implements Serializable {
4344
private static final SerializableFileFilter UNSUPPORTED_JVM_FILES_FILTER = SerializableFileFilter.skipFilesNamed(
4445
"package-info.java", "package-info.groovy", "module-info.java");
4546

46-
private final String licenseHeader;
47-
private final boolean hasYearToken;
48-
private final Pattern delimiterPattern;
49-
private final Pattern yearMatcherPattern;
50-
private final String licenseHeaderBeforeYearToken;
51-
private final String licenseHeaderAfterYearToken;
52-
private final String licenseHeaderWithYearTokenReplaced;
53-
5447
/** Creates a FormatterStep which forces the start of each file to match a license header. */
5548
public static FormatterStep createFromHeader(String licenseHeader, String delimiter) {
5649
return createFromHeader(licenseHeader, delimiter, DEFAULT_YEAR_DELIMITER);
@@ -83,7 +76,7 @@ public static FormatterStep createFromFile(File licenseHeaderFile, Charset encod
8376
Objects.requireNonNull(delimiter, "delimiter");
8477
Objects.requireNonNull(yearSeparator, "yearSeparator");
8578
return FormatterStep.createLazy(LicenseHeaderStep.NAME,
86-
() -> new LicenseHeaderStep(licenseHeaderFile, encoding, delimiter, yearSeparator),
79+
() -> new LicenseHeaderStep(new String(Files.readAllBytes(licenseHeaderFile.toPath()), encoding), delimiter, yearSeparator),
8780
step -> step::format);
8881
}
8982

@@ -99,8 +92,19 @@ public static SerializableFileFilter unsupportedJvmFilesFilter() {
9992
return UNSUPPORTED_JVM_FILES_FILTER;
10093
}
10194

102-
/** The license that we'd like enforced. */
95+
private final Pattern delimiterPattern;
96+
private final String yearSepOrFull;
97+
private final @Nullable String yearToday;
98+
private final @Nullable String beforeYear;
99+
private final @Nullable String afterYear;
100+
private final boolean updateYearWithLatest;
101+
103102
private LicenseHeaderStep(String licenseHeader, String delimiter, String yearSeparator) {
103+
this(licenseHeader, delimiter, yearSeparator, false);
104+
}
105+
106+
/** The license that we'd like enforced. */
107+
public LicenseHeaderStep(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest) {
104108
if (delimiter.contains("\n")) {
105109
throw new IllegalArgumentException("The delimiter must not contain any newlines.");
106110
}
@@ -109,25 +113,27 @@ private LicenseHeaderStep(String licenseHeader, String delimiter, String yearSep
109113
if (!licenseHeader.endsWith("\n")) {
110114
licenseHeader = licenseHeader + "\n";
111115
}
112-
this.licenseHeader = licenseHeader;
113116
this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE);
114117

115118
Optional<String> yearToken = getYearToken(licenseHeader);
116-
this.hasYearToken = yearToken.isPresent();
117-
if (hasYearToken) {
119+
if (yearToken.isPresent()) {
120+
yearToday = String.valueOf(YearMonth.now().getYear());
118121
int yearTokenIndex = licenseHeader.indexOf(yearToken.get());
119-
licenseHeaderBeforeYearToken = licenseHeader.substring(0, yearTokenIndex);
120-
licenseHeaderAfterYearToken = licenseHeader.substring(yearTokenIndex + 5);
121-
licenseHeaderWithYearTokenReplaced = licenseHeader.replace(yearToken.get(), String.valueOf(YearMonth.now().getYear()));
122-
yearMatcherPattern = Pattern.compile("[0-9]{4}(" + Pattern.quote(yearSeparator) + "[0-9]{4})?");
122+
beforeYear = licenseHeader.substring(0, yearTokenIndex);
123+
afterYear = licenseHeader.substring(yearTokenIndex + yearToken.get().length());
124+
yearSepOrFull = yearSeparator;
125+
this.updateYearWithLatest = updateYearWithLatest;
123126
} else {
124-
licenseHeaderBeforeYearToken = null;
125-
licenseHeaderAfterYearToken = null;
126-
licenseHeaderWithYearTokenReplaced = null;
127-
yearMatcherPattern = null;
127+
yearToday = null;
128+
beforeYear = null;
129+
afterYear = null;
130+
this.yearSepOrFull = licenseHeader;
131+
this.updateYearWithLatest = false;
128132
}
129133
}
130134

135+
private static final Pattern patternYearSingle = Pattern.compile("[0-9]{4}");
136+
131137
/**
132138
* Get the first place holder token being used in the
133139
* license header for specifying the year
@@ -139,39 +145,66 @@ private static Optional<String> getYearToken(String licenseHeader) {
139145
return YEAR_TOKENS.stream().filter(licenseHeader::contains).findFirst();
140146
}
141147

142-
/** Reads the license file from the given file. */
143-
private LicenseHeaderStep(File licenseFile, Charset encoding, String delimiter, String yearSeparator) throws IOException {
144-
this(new String(Files.readAllBytes(licenseFile.toPath()), encoding), delimiter, yearSeparator);
145-
}
146-
147148
/** Formats the given string. */
148149
public String format(String raw) {
149-
Matcher matcher = delimiterPattern.matcher(raw);
150-
if (!matcher.find()) {
150+
Matcher contentMatcher = delimiterPattern.matcher(raw);
151+
if (!contentMatcher.find()) {
151152
throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern);
152153
} else {
153-
if (hasYearToken) {
154-
if (matchesLicenseWithYearToken(raw, matcher)) {
155-
// that means we have the license like `licenseHeaderBeforeYearToken 1990-2015 licenseHeaderAfterYearToken`
154+
if (yearToday == null) {
155+
// the no year case is easy
156+
if (contentMatcher.start() == yearSepOrFull.length() && raw.startsWith(yearSepOrFull)) {
157+
// if no change is required, return the raw string without
158+
// creating any other new strings for maximum performance
156159
return raw;
157160
} else {
158-
return licenseHeaderWithYearTokenReplaced + raw.substring(matcher.start());
161+
// otherwise we'll have to add the header
162+
return yearSepOrFull + raw.substring(contentMatcher.start());
159163
}
160-
} else if (matcher.start() == licenseHeader.length() && raw.startsWith(licenseHeader)) {
161-
// if no change is required, return the raw string without
162-
// creating any other new strings for maximum performance
163-
return raw;
164164
} else {
165-
// otherwise we'll have to add the header
166-
return licenseHeader + raw.substring(matcher.start());
165+
// the yes year case is a bit harder
166+
int beforeYearIdx = raw.indexOf(beforeYear);
167+
int afterYearIdx = raw.indexOf(afterYear, beforeYearIdx + beforeYear.length() + 1);
168+
169+
if (beforeYearIdx >= 0 && afterYearIdx >= 0 && afterYearIdx + afterYear.length() <= contentMatcher.start()) {
170+
boolean noPadding = beforeYearIdx == 0 && afterYearIdx + afterYear.length() == contentMatcher.start(); // allows fastpath return raw
171+
String parsedYear = raw.substring(beforeYearIdx + beforeYear.length(), afterYearIdx);
172+
if (parsedYear.equals(yearToday)) {
173+
// it's good as is!
174+
return noPadding ? raw : beforeYear + yearToday + afterYear + raw.substring(contentMatcher.start());
175+
} else if (patternYearSingle.matcher(parsedYear).matches()) {
176+
if (updateYearWithLatest) {
177+
// expand from `2004` to `2004-2020`
178+
return beforeYear + parsedYear + yearSepOrFull + yearToday + afterYear + raw.substring(contentMatcher.start());
179+
} else {
180+
// it's already good as a single year
181+
return noPadding ? raw : beforeYear + parsedYear + afterYear + raw.substring(contentMatcher.start());
182+
}
183+
} else {
184+
Matcher yearMatcher = patternYearSingle.matcher(parsedYear);
185+
if (yearMatcher.find()) {
186+
String firstYear = yearMatcher.group();
187+
String newYear;
188+
String secondYear;
189+
if (updateYearWithLatest) {
190+
secondYear = firstYear.equals(yearToday) ? null : yearToday;
191+
} else if (yearMatcher.find(yearMatcher.end() + 1)) {
192+
secondYear = yearMatcher.group();
193+
} else {
194+
secondYear = null;
195+
}
196+
if (secondYear == null) {
197+
newYear = firstYear;
198+
} else {
199+
newYear = firstYear + yearSepOrFull + secondYear;
200+
}
201+
return noPadding && newYear.equals(parsedYear) ? raw : beforeYear + newYear + afterYear + raw.substring(contentMatcher.start());
202+
}
203+
}
204+
}
205+
// at worst, we just say that it was made today
206+
return beforeYear + yearToday + afterYear + raw.substring(contentMatcher.start());
167207
}
168208
}
169209
}
170-
171-
private boolean matchesLicenseWithYearToken(String raw, Matcher matcher) {
172-
int startOfTheSecondPart = raw.indexOf(licenseHeaderAfterYearToken);
173-
return startOfTheSecondPart > licenseHeaderBeforeYearToken.length()
174-
&& (raw.startsWith(licenseHeaderBeforeYearToken) && startOfTheSecondPart + licenseHeaderAfterYearToken.length() == matcher.start())
175-
&& yearMatcherPattern.matcher(raw.substring(licenseHeaderBeforeYearToken.length(), startOfTheSecondPart)).matches();
176-
}
177210
}

Diff for: plugin-gradle/CHANGES.md

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* If you use `ratchetFrom` and `licenseHeader`, the year in your license header will now be automatically kept up-to-date for changed files. For example, if the current year is 2020: ([#593](https://github.com/diffplug/spotless/pull/593))
8+
* `/** Copyright 2020 */` -> unchanged
9+
* `/** Copyright 1990 */` -> `/** Copyright 1990-2020 */`
10+
* `/** Copyright 1990-1993 */` -> `/** Copyright 1990-2020 */`
11+
* You can disable this behavior with `licenseHeader(...).updateYearWithLatest(false)`, or you can enable it without using `ratchetFrom` by using `updateYearWithLatest(true)` (not recommended).
612
### Fixed
713
* `ratchetFrom` had a bug (now fixed) such that it reported all files outside the root directory as changed. ([#594](https://github.com/diffplug/spotless/pull/594))
814

Diff for: plugin-gradle/README.md

+6-23
Original file line numberDiff line numberDiff line change
@@ -543,32 +543,15 @@ to true.
543543

544544
## License header options
545545

546-
If the license header (specified with `licenseHeader` or `licenseHeaderFile`) contains `$YEAR` or `$today.year`, then that token will be replaced with the current 4-digit year. For example, if Spotless is launched in 2017, then `/* Licensed under Apache-2.0 $YEAR. */` will produce `/* Licensed under Apache-2.0 2017. */`
546+
If the license header (specified with `licenseHeader` or `licenseHeaderFile`) contains `$YEAR` or `$today.year`, then that token will be replaced with the current 4-digit year. For example, if Spotless is launched in 2020, then `/* Licensed under Apache-2.0 $YEAR. */` will produce `/* Licensed under Apache-2.0 2020. */`
547547

548-
The `licenseHeader` and `licenseHeaderFile` steps will generate license headers with automatic years according to the following rules:
549-
* A generated license header will be updated with the current year when
550-
* the generated license header is missing
551-
* the generated license header is not formatted correctly
552-
* A generated license header will _not_ be updated when
553-
* a single year is already present, e.g.
554-
`/* Licensed under Apache-2.0 1990. */`
555-
* a year range is already present, e.g.
556-
`/* Licensed under Apache-2.0 1990-2003. */`
557-
* the `$YEAR` token is otherwise missing
548+
Once a file's license header has a valid year, whether it is a year (`2020`) or a year range (`2017-2020`), it will not be changed. If you want the date to be updated when it changes, enable the [`ratchetFrom` functionality](#ratchet), and the year will be automatically set to today's year according to the following table (assuming the current year is 2020):
558549

559-
The separator for the year range defaults to the hyphen character, e.g `1990-2003`, but can be customized with the `yearSeparator` property.
550+
* No license header -> `2020`
551+
* `2017` -> `2017-2020`
552+
* `2017-2019` -> `2017-2020`
560553

561-
For instance, the following configuration treats `1990, 2003` as a valid year range.
562-
563-
```gradle
564-
spotless {
565-
java {
566-
licenseHeader('Licensed under Apache-2.0 $YEAR').yearSeparator(', ')
567-
}
568-
}
569-
```
570-
571-
To update the copyright notice only for changed files, use the [`ratchetFrom` functionality](#ratchet).
554+
See the [javadoc](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/4.1.0/com/diffplug/gradle/spotless/FormatExtension.LicenseHeaderConfig.html) for a complete listing of options.
572555

573556
<a name="custom"></a>
574557

Diff for: plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java

+37-11
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull;
1919

2020
import java.io.File;
21+
import java.io.IOException;
2122
import java.io.Serializable;
2223
import java.nio.charset.Charset;
24+
import java.nio.file.Files;
2325
import java.util.*;
2426
import java.util.stream.Stream;
2527

@@ -406,9 +408,15 @@ public void indentWithTabs() {
406408
addStep(IndentStep.Type.TAB.create());
407409
}
408410

411+
/**
412+
* Created by {@link FormatExtension#licenseHeader(String, String)} or {@link FormatExtension#licenseHeaderFile(Object, String)}.
413+
* For most language-specific formats (e.g. java, scala, etc.) you can omit the second `delimiter` argument, because it is supplied
414+
* automatically ({@link HasBuiltinDelimiterForLicense}).
415+
*/
409416
public abstract class LicenseHeaderConfig {
410417
String delimiter;
411418
String yearSeparator = LicenseHeaderStep.defaultYearDelimiter();
419+
Boolean updateYearWithLatest = null;
412420

413421
public LicenseHeaderConfig(String delimiter) {
414422
this.delimiter = Objects.requireNonNull(delimiter, "delimiter");
@@ -434,36 +442,54 @@ public LicenseHeaderConfig yearSeparator(String yearSeparator) {
434442
return this;
435443
}
436444

437-
abstract FormatterStep createStep();
438-
}
445+
/**
446+
* @param updateYearWithLatest
447+
* Will turn `2004` into `2004-2020`, and `2004-2019` into `2004-2020`
448+
* Default value is false, unless {@link SpotlessExtension#ratchetFrom(String)} is used, in which case default value is true.
449+
*/
450+
public LicenseHeaderConfig updateYearWithLatest(boolean overwriteYearLatest) {
451+
this.updateYearWithLatest = overwriteYearLatest;
452+
replaceStep(createStep());
453+
return this;
454+
}
439455

440-
public class LicenseStringHeaderConfig extends LicenseHeaderConfig {
456+
protected abstract String licenseHeader() throws IOException;
441457

458+
FormatterStep createStep() {
459+
return FormatterStep.createLazy(LicenseHeaderStep.name(), () -> {
460+
// by default, we should update the year if the user is using ratchetFrom
461+
boolean updateYear = updateYearWithLatest == null ? FormatExtension.this.root.getRatchetFrom() != null : updateYearWithLatest;
462+
return new LicenseHeaderStep(licenseHeader(), delimiter, yearSeparator, updateYear);
463+
}, step -> step::format);
464+
}
465+
}
466+
467+
private class LicenseStringHeaderConfig extends LicenseHeaderConfig {
442468
private String header;
443469

444470
LicenseStringHeaderConfig(String delimiter, String header) {
445471
super(delimiter);
446472
this.header = Objects.requireNonNull(header, "header");
447473
}
448474

449-
FormatterStep createStep() {
450-
return LicenseHeaderStep.createFromHeader(header, delimiter, yearSeparator);
475+
@Override
476+
protected String licenseHeader() {
477+
return header;
451478
}
452479
}
453480

454-
public class LicenseFileHeaderConfig extends LicenseHeaderConfig {
455-
481+
private class LicenseFileHeaderConfig extends LicenseHeaderConfig {
456482
private Object headerFile;
457483

458484
LicenseFileHeaderConfig(String delimiter, Object headerFile) {
459485
super(delimiter);
460486
this.headerFile = Objects.requireNonNull(headerFile, "headerFile");
461487
}
462488

463-
FormatterStep createStep() {
464-
return LicenseHeaderStep
465-
.createFromFile(getProject().file(headerFile), getEncoding(), delimiter,
466-
yearSeparator);
489+
@Override
490+
protected String licenseHeader() throws IOException {
491+
byte[] content = Files.readAllBytes(getProject().file(headerFile).toPath());
492+
return new String(content, getEncoding());
467493
}
468494
}
469495

Diff for: plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919

2020
import java.io.IOException;
21-
import java.time.YearMonth;
2221

2322
import org.gradle.testkit.runner.BuildResult;
2423
import org.junit.Test;
@@ -193,7 +192,7 @@ public void testWithNonStandardYearSeparator() throws IOException {
193192
matcher.startsWith("// License Header 2012, 2014");
194193
});
195194
assertFile("src/main/kotlin/test2.kt").matches(matcher -> {
196-
matcher.startsWith(HEADER_WITH_YEAR.replace("$YEAR", String.valueOf(YearMonth.now().getYear())));
195+
matcher.startsWith("// License Header 2012, 2014");
197196
});
198197
}
199198

@@ -223,7 +222,7 @@ public void testWithNonStandardYearSeparatorKtfmt() throws IOException {
223222
matcher.startsWith("// License Header 2012, 2014");
224223
});
225224
assertFile("src/main/kotlin/test2.kt").matches(matcher -> {
226-
matcher.startsWith(HEADER_WITH_YEAR.replace("$YEAR", String.valueOf(YearMonth.now().getYear())));
225+
matcher.startsWith("// License Header 2012, 2014");
227226
});
228227
}
229228
}

0 commit comments

Comments
 (0)