Skip to content

Commit 1effefb

Browse files
authored
Support for skipping lines before license header (#1441)
2 parents 308c7cc + 68e7099 commit 1effefb

File tree

11 files changed

+113
-22
lines changed

11 files changed

+113
-22
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ 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+
* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441)).
1315
### Fixed
1416
* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444))
17+
1518
### Changes
1619
* Bump the dev version of Gradle from `7.5.1` to `7.6` ([#1409](https://github.com/diffplug/spotless/pull/1409))
1720
* We also removed the no-longer-required dependency `org.codehaus.groovy:groovy-xml`

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

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 DiffPlug
2+
* Copyright 2016-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ public static LicenseHeaderStep headerDelimiter(String header, String delimiter)
5151
}
5252

5353
public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier<String> headerLazy, String delimiter) {
54-
return new LicenseHeaderStep(null, null, headerLazy, delimiter, DEFAULT_YEAR_DELIMITER, () -> YearMode.PRESERVE);
54+
return new LicenseHeaderStep(null, null, headerLazy, delimiter, DEFAULT_YEAR_DELIMITER, () -> YearMode.PRESERVE, null);
5555
}
5656

5757
final String name;
@@ -60,50 +60,56 @@ public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier<String> head
6060
final String delimiter;
6161
final String yearSeparator;
6262
final Supplier<YearMode> yearMode;
63+
final @Nullable String skipLinesMatching;
6364

64-
private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier<String> headerLazy, String delimiter, String yearSeparator, Supplier<YearMode> yearMode) {
65+
private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier<String> headerLazy, String delimiter, String yearSeparator, Supplier<YearMode> yearMode, @Nullable String skipLinesMatching) {
6566
this.name = sanitizeName(name);
66-
this.contentPattern = sanitizeContentPattern(contentPattern);
67+
this.contentPattern = sanitizePattern(contentPattern);
6768
this.headerLazy = Objects.requireNonNull(headerLazy);
6869
this.delimiter = Objects.requireNonNull(delimiter);
6970
this.yearSeparator = Objects.requireNonNull(yearSeparator);
7071
this.yearMode = Objects.requireNonNull(yearMode);
72+
this.skipLinesMatching = sanitizePattern(skipLinesMatching);
7173
}
7274

7375
public String getName() {
7476
return name;
7577
}
7678

7779
public LicenseHeaderStep withName(String name) {
78-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
80+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
7981
}
8082

8183
public LicenseHeaderStep withContentPattern(String contentPattern) {
82-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
84+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
8385
}
8486

8587
public LicenseHeaderStep withHeaderString(String header) {
8688
return withHeaderLazy(() -> header);
8789
}
8890

8991
public LicenseHeaderStep withHeaderLazy(ThrowingEx.Supplier<String> headerLazy) {
90-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
92+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
9193
}
9294

9395
public LicenseHeaderStep withDelimiter(String delimiter) {
94-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
96+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
9597
}
9698

9799
public LicenseHeaderStep withYearSeparator(String yearSeparator) {
98-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
100+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
99101
}
100102

101103
public LicenseHeaderStep withYearMode(YearMode yearMode) {
102104
return withYearModeLazy(() -> yearMode);
103105
}
104106

105107
public LicenseHeaderStep withYearModeLazy(Supplier<YearMode> yearMode) {
106-
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
108+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
109+
}
110+
111+
public LicenseHeaderStep withSkipLinesMatching(@Nullable String skipLinesMatching) {
112+
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
107113
}
108114

109115
public FormatterStep build() {
@@ -112,7 +118,7 @@ public FormatterStep build() {
112118
if (yearMode.get() == YearMode.SET_FROM_GIT) {
113119
formatterStep = FormatterStep.createNeverUpToDateLazy(name, () -> {
114120
boolean updateYear = false; // doesn't matter
115-
Runtime runtime = new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear);
121+
Runtime runtime = new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching);
116122
return FormatterFunc.needsFile(runtime::setLicenseHeaderYearsFromGitHistory);
117123
});
118124
} else {
@@ -130,7 +136,7 @@ public FormatterStep build() {
130136
default:
131137
throw new IllegalStateException(yearMode.toString());
132138
}
133-
return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear);
139+
return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching);
134140
}, step -> step::format);
135141
}
136142

@@ -156,18 +162,18 @@ private String sanitizeName(@Nullable String name) {
156162
}
157163

158164
@Nullable
159-
private String sanitizeContentPattern(@Nullable String contentPattern) {
160-
if (contentPattern == null) {
161-
return contentPattern;
165+
private String sanitizePattern(@Nullable String pattern) {
166+
if (pattern == null) {
167+
return pattern;
162168
}
163169

164-
contentPattern = contentPattern.trim();
170+
pattern = pattern.trim();
165171

166-
if (contentPattern.isEmpty()) {
172+
if (pattern.isEmpty()) {
167173
return null;
168174
}
169175

170-
return contentPattern;
176+
return pattern;
171177
}
172178

173179
private static final String DEFAULT_NAME_PREFIX = LicenseHeaderStep.class.getName();
@@ -195,6 +201,7 @@ private static class Runtime implements Serializable {
195201
private static final long serialVersionUID = 1475199492829130965L;
196202

197203
private final Pattern delimiterPattern;
204+
private final @Nullable Pattern skipLinesMatching;
198205
private final String yearSepOrFull;
199206
private final @Nullable String yearToday;
200207
private final @Nullable String beforeYear;
@@ -203,7 +210,7 @@ private static class Runtime implements Serializable {
203210
private final boolean licenseHeaderWithRange;
204211

205212
/** The license that we'd like enforced. */
206-
private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest) {
213+
private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest, @Nullable String skipLinesMatching) {
207214
if (delimiter.contains("\n")) {
208215
throw new IllegalArgumentException("The delimiter must not contain any newlines.");
209216
}
@@ -213,6 +220,7 @@ private Runtime(String licenseHeader, String delimiter, String yearSeparator, bo
213220
licenseHeader = licenseHeader + "\n";
214221
}
215222
this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE);
223+
this.skipLinesMatching = skipLinesMatching == null ? null : Pattern.compile(skipLinesMatching);
216224

217225
Optional<String> yearToken = getYearToken(licenseHeader);
218226
if (yearToken.isPresent()) {
@@ -254,6 +262,31 @@ private static Optional<String> getYearToken(String licenseHeader) {
254262

255263
/** Formats the given string. */
256264
private String format(String raw) {
265+
if (skipLinesMatching == null) {
266+
return addOrUpdateLicenseHeader(raw);
267+
} else {
268+
String[] lines = raw.split("\n");
269+
StringBuilder skippedLinesBuilder = new StringBuilder();
270+
StringBuilder remainingLinesBuilder = new StringBuilder();
271+
boolean lastMatched = true;
272+
for (String line : lines) {
273+
if (lastMatched) {
274+
Matcher matcher = skipLinesMatching.matcher(line);
275+
if (matcher.find()) {
276+
skippedLinesBuilder.append(line).append('\n');
277+
} else {
278+
remainingLinesBuilder.append(line).append('\n');
279+
lastMatched = false;
280+
}
281+
} else {
282+
remainingLinesBuilder.append(line).append('\n');
283+
}
284+
}
285+
return skippedLinesBuilder + addOrUpdateLicenseHeader(remainingLinesBuilder.toString());
286+
}
287+
}
288+
289+
private String addOrUpdateLicenseHeader(String raw) {
257290
Matcher contentMatcher = delimiterPattern.matcher(raw);
258291
if (!contentMatcher.find()) {
259292
throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern);

plugin-gradle/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
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+
* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441))
68
### Fixed
79
* Prevent tool configurations from being resolved outside project ([#1447](https://github.com/diffplug/spotless/pull/1447) fixes [#1215](https://github.com/diffplug/spotless/issues/1215))
810
* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444))

plugin-gradle/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,12 @@ See the [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-g
843843
844844
If your project has not been rigorous with copyright headers, and you'd like to use git history to repair this retroactively, you can do so with `-PspotlessSetLicenseHeaderYearsFromGitHistory=true`. When run in this mode, Spotless will do an expensive search through git history for each file, and set the copyright header based on the oldest and youngest commits for that file. This is intended to be a one-off sort of thing.
845845
846+
### Files with fixed header lines
847+
848+
Some files have fixed header lines (e.g. `<?xml version="1.0" ...` in XMLs, or `#!/bin/bash` in bash scripts). Comments cannot precede these, so the license header has to come after them, too.
849+
850+
To define what lines to skip at the beginning of such files, fill the `skipLinesMatching` option with a regular expression that matches them (e.g. `.skipLinesMatching("^#!.+?\$")` to skip shebangs).
851+
846852
<a name="ratchet"></a>
847853
848854
## How can I enforce formatting gradually? (aka "ratchet")

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2022 DiffPlug
2+
* Copyright 2016-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -462,6 +462,12 @@ public LicenseHeaderConfig yearSeparator(String yearSeparator) {
462462
return this;
463463
}
464464

465+
public LicenseHeaderConfig skipLinesMatching(String skipLinesMatching) {
466+
builder = builder.withSkipLinesMatching(skipLinesMatching);
467+
replaceStep(createStep());
468+
return this;
469+
}
470+
465471
/**
466472
* @param updateYearWithLatest
467473
* Will turn {@code 2004} into {@code 2004-2020}, and {@code 2004-2019} into {@code 2004-2020}

plugin-maven/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441))
68
### Fixed
79
* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444))
810
### Changes

plugin-maven/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@ Once a file's license header has a valid year, whether it is a year (`2020`) or
10131013

10141014
If your project has not been rigorous with copyright headers, and you'd like to use git history to repair this retroactively, you can do so with `-DspotlessSetLicenseHeaderYearsFromGitHistory=true`. When run in this mode, Spotless will do an expensive search through git history for each file, and set the copyright header based on the oldest and youngest commits for that file. This is intended to be a one-off sort of thing.
10151015

1016+
### Files with fixed header lines
1017+
1018+
Some files have fixed header lines (e.g. `<?xml version="1.0" ...` in XMLs, or `#!/bin/bash` in bash scripts). Comments cannot precede these, so the license header has to come after them, too.
1019+
1020+
To define what lines to skip at the beginning of such files, fill the `skipLinesMatching` option with a regular expression that matches them (e.g. `<skipLinesMatching>^#!.+?$</skipLinesMatching>` to skip shebangs).
1021+
10161022
<a name="invisible"></a>
10171023

10181024
<a name="ratchet"></a>

plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 DiffPlug
2+
* Copyright 2016-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,9 @@ public class LicenseHeader implements FormatterStepFactory {
3737
@Parameter
3838
private String delimiter;
3939

40+
@Parameter
41+
private String skipLinesMatching;
42+
4043
@Override
4144
public final FormatterStep newFormatterStep(FormatterStepConfig config) {
4245
String delimiterString = delimiter != null ? delimiter : config.getLicenseHeaderDelimiter();
@@ -53,6 +56,7 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) {
5356
}
5457
return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString)
5558
.withYearMode(yearMode)
59+
.withSkipLinesMatching(skipLinesMatching)
5660
.build()
5761
.filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter());
5862
} else {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
3+
4+
<module name="Checker">
5+
<module name="ThisIsNotARealCheckstyleConfigFolks">
6+
<property name="goodAdvice" value="dontTryItAnakin"/>
7+
<property name="adviceGiver" value="generalKenobi"/>
8+
</module>
9+
</module>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
3+
<!--
4+
-- This is a fake license header.
5+
-- All rights reserved.
6+
-->
7+
8+
<module name="Checker">
9+
<module name="ThisIsNotARealCheckstyleConfigFolks">
10+
<property name="goodAdvice" value="dontTryItAnakin"/>
11+
<property name="adviceGiver" value="generalKenobi"/>
12+
</module>
13+
</module>

0 commit comments

Comments
 (0)