Skip to content

Commit

Permalink
Support for skipping lines before license header (#1441)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg committed Jan 8, 2023
2 parents 308c7cc + 68e7099 commit 1effefb
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ 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]
### Added
* 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)).
### Fixed
* 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))

### Changes
* Bump the dev version of Gradle from `7.5.1` to `7.6` ([#1409](https://github.com/diffplug/spotless/pull/1409))
* We also removed the no-longer-required dependency `org.codehaus.groovy:groovy-xml`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2021 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,7 +51,7 @@ public static LicenseHeaderStep headerDelimiter(String header, String delimiter)
}

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

final String name;
Expand All @@ -60,50 +60,56 @@ public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier<String> head
final String delimiter;
final String yearSeparator;
final Supplier<YearMode> yearMode;
final @Nullable String skipLinesMatching;

private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier<String> headerLazy, String delimiter, String yearSeparator, Supplier<YearMode> yearMode) {
private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier<String> headerLazy, String delimiter, String yearSeparator, Supplier<YearMode> yearMode, @Nullable String skipLinesMatching) {
this.name = sanitizeName(name);
this.contentPattern = sanitizeContentPattern(contentPattern);
this.contentPattern = sanitizePattern(contentPattern);
this.headerLazy = Objects.requireNonNull(headerLazy);
this.delimiter = Objects.requireNonNull(delimiter);
this.yearSeparator = Objects.requireNonNull(yearSeparator);
this.yearMode = Objects.requireNonNull(yearMode);
this.skipLinesMatching = sanitizePattern(skipLinesMatching);
}

public String getName() {
return name;
}

public LicenseHeaderStep withName(String name) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withContentPattern(String contentPattern) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withHeaderString(String header) {
return withHeaderLazy(() -> header);
}

public LicenseHeaderStep withHeaderLazy(ThrowingEx.Supplier<String> headerLazy) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withDelimiter(String delimiter) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withYearSeparator(String yearSeparator) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withYearMode(YearMode yearMode) {
return withYearModeLazy(() -> yearMode);
}

public LicenseHeaderStep withYearModeLazy(Supplier<YearMode> yearMode) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode);
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public LicenseHeaderStep withSkipLinesMatching(@Nullable String skipLinesMatching) {
return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching);
}

public FormatterStep build() {
Expand All @@ -112,7 +118,7 @@ public FormatterStep build() {
if (yearMode.get() == YearMode.SET_FROM_GIT) {
formatterStep = FormatterStep.createNeverUpToDateLazy(name, () -> {
boolean updateYear = false; // doesn't matter
Runtime runtime = new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear);
Runtime runtime = new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching);
return FormatterFunc.needsFile(runtime::setLicenseHeaderYearsFromGitHistory);
});
} else {
Expand All @@ -130,7 +136,7 @@ public FormatterStep build() {
default:
throw new IllegalStateException(yearMode.toString());
}
return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear);
return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching);
}, step -> step::format);
}

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

@Nullable
private String sanitizeContentPattern(@Nullable String contentPattern) {
if (contentPattern == null) {
return contentPattern;
private String sanitizePattern(@Nullable String pattern) {
if (pattern == null) {
return pattern;
}

contentPattern = contentPattern.trim();
pattern = pattern.trim();

if (contentPattern.isEmpty()) {
if (pattern.isEmpty()) {
return null;
}

return contentPattern;
return pattern;
}

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

private final Pattern delimiterPattern;
private final @Nullable Pattern skipLinesMatching;
private final String yearSepOrFull;
private final @Nullable String yearToday;
private final @Nullable String beforeYear;
Expand All @@ -203,7 +210,7 @@ private static class Runtime implements Serializable {
private final boolean licenseHeaderWithRange;

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

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

/** Formats the given string. */
private String format(String raw) {
if (skipLinesMatching == null) {
return addOrUpdateLicenseHeader(raw);
} else {
String[] lines = raw.split("\n");
StringBuilder skippedLinesBuilder = new StringBuilder();
StringBuilder remainingLinesBuilder = new StringBuilder();
boolean lastMatched = true;
for (String line : lines) {
if (lastMatched) {
Matcher matcher = skipLinesMatching.matcher(line);
if (matcher.find()) {
skippedLinesBuilder.append(line).append('\n');
} else {
remainingLinesBuilder.append(line).append('\n');
lastMatched = false;
}
} else {
remainingLinesBuilder.append(line).append('\n');
}
}
return skippedLinesBuilder + addOrUpdateLicenseHeader(remainingLinesBuilder.toString());
}
}

private String addOrUpdateLicenseHeader(String raw) {
Matcher contentMatcher = delimiterPattern.matcher(raw);
if (!contentMatcher.find()) {
throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern);
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]
### Added
* 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))
### Fixed
* 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))
* 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))
Expand Down
6 changes: 6 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,12 @@ See the [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-g
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.
### Files with fixed header lines
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.
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).
<a name="ratchet"></a>
## How can I enforce formatting gradually? (aka "ratchet")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -462,6 +462,12 @@ public LicenseHeaderConfig yearSeparator(String yearSeparator) {
return this;
}

public LicenseHeaderConfig skipLinesMatching(String skipLinesMatching) {
builder = builder.withSkipLinesMatching(skipLinesMatching);
replaceStep(createStep());
return this;
}

/**
* @param updateYearWithLatest
* Will turn {@code 2004} into {@code 2004-2020}, and {@code 2004-2019} into {@code 2004-2020}
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]
### Added
* 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))
### Fixed
* 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))
### Changes
Expand Down
6 changes: 6 additions & 0 deletions plugin-maven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,12 @@ Once a file's license header has a valid year, whether it is a year (`2020`) or

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.

### Files with fixed header lines

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.

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).

<a name="invisible"></a>

<a name="ratchet"></a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,6 +37,9 @@ public class LicenseHeader implements FormatterStepFactory {
@Parameter
private String delimiter;

@Parameter
private String skipLinesMatching;

@Override
public final FormatterStep newFormatterStep(FormatterStepConfig config) {
String delimiterString = delimiter != null ? delimiter : config.getLicenseHeaderDelimiter();
Expand All @@ -53,6 +56,7 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) {
}
return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString)
.withYearMode(yearMode)
.withSkipLinesMatching(skipLinesMatching)
.build()
.filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter());
} else {
Expand Down
9 changes: 9 additions & 0 deletions testlib/src/main/resources/license/SkipLines.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">

<module name="Checker">
<module name="ThisIsNotARealCheckstyleConfigFolks">
<property name="goodAdvice" value="dontTryItAnakin"/>
<property name="adviceGiver" value="generalKenobi"/>
</module>
</module>
13 changes: 13 additions & 0 deletions testlib/src/main/resources/license/SkipLinesHasLicense.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
-- This is a fake license header.
-- All rights reserved.
-->

<module name="Checker">
<module name="ThisIsNotARealCheckstyleConfigFolks">
<property name="goodAdvice" value="dontTryItAnakin"/>
<property name="adviceGiver" value="generalKenobi"/>
</module>
</module>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -122,6 +122,13 @@ void should_remove_header_when_empty() throws Throwable {
.test(getTestResource("license/HasLicense.test"), getTestResource("license/MissingLicense.test"));
}

@Test
void should_skip_lines_matching_predefined_pattern() throws Throwable {
StepHarness.forStep(LicenseHeaderStep.headerDelimiter("<!--\n -- This is a fake license header.\n -- All rights reserved.\n -->", "^(?!<!--|\\s+--).*$")
.withSkipLinesMatching("(?i)^(<\\?xml[^>]+>|<!doctype[^>]+>)$").build())
.testResource("license/SkipLines.test", "license/SkipLinesHasLicense.test");
}

private String licenceWithAddress() {
return "Copyright &#169; $YEAR FooBar Inc. All Rights Reserved.\n" +
" *\n" +
Expand Down

0 comments on commit 1effefb

Please sign in to comment.