Skip to content

Commit 4ce31f0

Browse files
authored
Merge pull request #179 from baptistemesta/feat/handle_year_in_copyright
add a $YEAR as a supported variable in license
2 parents 3ebb666 + d71cfd7 commit 4ce31f0

File tree

6 files changed

+149
-1
lines changed

6 files changed

+149
-1
lines changed

lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.Serializable;
2121
import java.nio.charset.Charset;
2222
import java.nio.file.Files;
23+
import java.time.YearMonth;
2324
import java.util.Objects;
2425
import java.util.regex.Matcher;
2526
import java.util.regex.Pattern;
@@ -35,6 +36,11 @@ public final class LicenseHeaderStep implements Serializable {
3536

3637
private final String licenseHeader;
3738
private final Pattern delimiterPattern;
39+
private Pattern yearMatcherPattern;
40+
private boolean hasYearToken;
41+
private String licenseHeaderBeforeYearToken;
42+
private String licenseHeaderAfterYearToken;
43+
private String licenseHeaderWithYearTokenReplaced;
3844

3945
/** Creates a FormatterStep which forces the start of each file to match a license header. */
4046
public static FormatterStep createFromHeader(String licenseHeader, String delimiter) {
@@ -74,6 +80,14 @@ private LicenseHeaderStep(String licenseHeader, String delimiter) {
7480
}
7581
this.licenseHeader = licenseHeader;
7682
this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE);
83+
this.hasYearToken = licenseHeader.contains("$YEAR");
84+
if (this.hasYearToken) {
85+
int yearTokenIndex = licenseHeader.indexOf("$YEAR");
86+
this.licenseHeaderBeforeYearToken = licenseHeader.substring(0, yearTokenIndex);
87+
this.licenseHeaderAfterYearToken = licenseHeader.substring(yearTokenIndex + 5, licenseHeader.length());
88+
this.licenseHeaderWithYearTokenReplaced = licenseHeader.replace("$YEAR", String.valueOf(YearMonth.now().getYear()));
89+
this.yearMatcherPattern = Pattern.compile("[0-9]{4}(-[0-9]{4})?");
90+
}
7791
}
7892

7993
/** Reads the license file from the given file. */
@@ -87,7 +101,14 @@ public String format(String raw) {
87101
if (!matcher.find()) {
88102
throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern);
89103
} else {
90-
if (matcher.start() == licenseHeader.length() && raw.startsWith(licenseHeader)) {
104+
if (hasYearToken) {
105+
if (matchesLicenseWithYearToken(raw, matcher)) {
106+
// that means we have the license like `licenseHeaderBeforeYearToken 1990-2015 licenseHeaderAfterYearToken`
107+
return raw;
108+
} else {
109+
return licenseHeaderWithYearTokenReplaced + raw.substring(matcher.start());
110+
}
111+
} else if (matcher.start() == licenseHeader.length() && raw.startsWith(licenseHeader)) {
91112
// if no change is required, return the raw string without
92113
// creating any other new strings for maximum performance
93114
return raw;
@@ -97,4 +118,10 @@ public String format(String raw) {
97118
}
98119
}
99120
}
121+
122+
private boolean matchesLicenseWithYearToken(String raw, Matcher matcher) {
123+
int startOfTheSecondPart = raw.indexOf(licenseHeaderAfterYearToken);
124+
return (raw.startsWith(licenseHeaderBeforeYearToken) && startOfTheSecondPart + licenseHeaderAfterYearToken.length() == matcher.start())
125+
&& yearMatcherPattern.matcher(raw.substring(licenseHeaderBeforeYearToken.length(), startOfTheSecondPart)).matches();
126+
}
100127
}

plugin-gradle/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,37 @@ spotless {
273273
}
274274
```
275275

276+
<a name="license-header"></a>
277+
278+
## License header options
279+
280+
If the string contents of a licenseHeader step or the file contents of a licenseHeaderFile step contains a $YEAR token,
281+
then in the end-result generated license headers which use this license header as a template, $YEAR will be replaced with the current year.
282+
283+
284+
For example:
285+
```
286+
/* Licensed under Apache-2.0 $YEAR. */
287+
```
288+
will produce
289+
```
290+
/* Licensed under Apache-2.0 2017. */
291+
```
292+
if Spotless is launched in 2017
293+
294+
295+
The `licenseHeader` and `licenseHeaderFile` steps will generate license headers with automatic years from the base license header according to the following rules:
296+
* A generated license header will be updated with the current year when
297+
* the generated license header is missing
298+
* the generated license header is not formatted correctly
299+
* A generated license header will _not_ be updated when
300+
* a single year is already present, e.g.
301+
`/* Licensed under Apache-2.0 1990. */`
302+
* a hyphen-separated year range is already present, e.g.
303+
`/* Licensed under Apache-2.0 1990-2003. */`
304+
* the `$YEAR` token is otherwise missing
305+
306+
276307
<a name="custom"></a>
277308

278309
## Custom rules

testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java

+28
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,28 @@
1616
package com.diffplug.spotless.generic;
1717

1818
import java.io.File;
19+
import java.io.IOException;
1920
import java.nio.charset.StandardCharsets;
21+
import java.time.YearMonth;
2022

2123
import org.junit.Assert;
2224
import org.junit.Test;
2325

2426
import com.diffplug.spotless.FormatterStep;
2527
import com.diffplug.spotless.ResourceHarness;
2628
import com.diffplug.spotless.SerializableEqualityTester;
29+
import com.diffplug.spotless.StepHarness;
2730

2831
public class LicenseHeaderStepTest extends ResourceHarness {
2932
private static final String KEY_LICENSE = "license/TestLicense";
3033
private static final String KEY_FILE_NOTAPPLIED = "license/MissingLicense.test";
3134
private static final String KEY_FILE_APPLIED = "license/HasLicense.test";
3235

36+
// files to test $YEAR token replacement
37+
private static final String KEY_LICENSE_WITH_YEAR_TOKEN = "license/LicenseHeaderWithYearToken";
38+
private static final String KEY_FILE_WITHOUT_LICENSE = "license/FileWithoutLicenseHeader.test";
39+
private static final String KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER = "license/FileWithLicenseHeaderAndPlaceholder.test";
40+
3341
// If this constant changes, don't forget to change the similarly-named one in
3442
// plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java as well
3543
private static final String LICENSE_HEADER_DELIMITER = "package ";
@@ -46,6 +54,26 @@ public void fromFile() throws Throwable {
4654
assertOnResources(step, KEY_FILE_NOTAPPLIED, KEY_FILE_APPLIED);
4755
}
4856

57+
@Test
58+
public void should_apply_license_containing_YEAR_token() throws Throwable {
59+
FormatterStep step = LicenseHeaderStep.createFromFile(createTestFile(KEY_LICENSE_WITH_YEAR_TOKEN), StandardCharsets.UTF_8, LICENSE_HEADER_DELIMITER);
60+
61+
StepHarness.forStep(step)
62+
.test(getTestResource(KEY_FILE_WITHOUT_LICENSE), fileWithPlaceholderContaining(currentYear()))
63+
.testUnaffected(fileWithPlaceholderContaining(currentYear()))
64+
.testUnaffected(fileWithPlaceholderContaining("2003"))
65+
.testUnaffected(fileWithPlaceholderContaining("1990-2015"))
66+
.test(fileWithPlaceholderContaining("not a year"), fileWithPlaceholderContaining(currentYear()));
67+
}
68+
69+
private String fileWithPlaceholderContaining(String placeHolderContent) throws IOException {
70+
return getTestResource(KEY_FILE_WITH_LICENSE_AND_PLACEHOLDER).replace("__PLACEHOLDER__", placeHolderContent);
71+
}
72+
73+
private String currentYear() {
74+
return String.valueOf(YearMonth.now().getYear());
75+
}
76+
4977
@Test
5078
public void efficient() throws Throwable {
5179
FormatterStep step = LicenseHeaderStep.createFromHeader("LicenseHeader\n", "contentstart");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* This is a fake license, __PLACEHOLDER__. ACME corp.
3+
**/
4+
package com.acme;
5+
6+
import java.util.function.Function;
7+
8+
9+
public class Java8Test {
10+
public void doStuff() throws Exception {
11+
Function<String, Integer> example = Integer::parseInt;
12+
example.andThen(val -> {
13+
return val + 2;
14+
} );
15+
SimpleEnum val = SimpleEnum.A;
16+
switch (val) {
17+
case A:
18+
break;
19+
case B:
20+
break;
21+
case C:
22+
break;
23+
default:
24+
throw new Exception();
25+
}
26+
}
27+
28+
public enum SimpleEnum {
29+
A, B, C;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.acme;
2+
3+
import java.util.function.Function;
4+
5+
6+
public class Java8Test {
7+
public void doStuff() throws Exception {
8+
Function<String, Integer> example = Integer::parseInt;
9+
example.andThen(val -> {
10+
return val + 2;
11+
} );
12+
SimpleEnum val = SimpleEnum.A;
13+
switch (val) {
14+
case A:
15+
break;
16+
case B:
17+
break;
18+
case C:
19+
break;
20+
default:
21+
throw new Exception();
22+
}
23+
}
24+
25+
public enum SimpleEnum {
26+
A, B, C;
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/*
2+
* This is a fake license, $YEAR. ACME corp.
3+
**/

0 commit comments

Comments
 (0)