diff --git a/CHANGES.md b/CHANGES.md index 50e4d83141..c7cde364c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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` diff --git a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java index 6a34c08828..46312ca07a 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java @@ -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. @@ -51,7 +51,7 @@ public static LicenseHeaderStep headerDelimiter(String header, String delimiter) } public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier 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; @@ -60,14 +60,16 @@ public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier head final String delimiter; final String yearSeparator; final Supplier yearMode; + final @Nullable String skipLinesMatching; - private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier yearMode) { + private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier 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() { @@ -75,11 +77,11 @@ public String getName() { } 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) { @@ -87,15 +89,15 @@ public LicenseHeaderStep withHeaderString(String header) { } public LicenseHeaderStep withHeaderLazy(ThrowingEx.Supplier 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) { @@ -103,7 +105,11 @@ public LicenseHeaderStep withYearMode(YearMode yearMode) { } public LicenseHeaderStep withYearModeLazy(Supplier 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() { @@ -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 { @@ -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); } @@ -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(); @@ -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; @@ -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."); } @@ -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 yearToken = getYearToken(licenseHeader); if (yearToken.isPresent()) { @@ -254,6 +262,31 @@ private static Optional 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); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 56ed8d6c1e..c887e820a6 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -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)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 755d21d056..7d465007b6 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -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. ` ## How can I enforce formatting gradually? (aka "ratchet") diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index de0313eab4..b8344c385f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -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. @@ -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} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 40038418b7..984c86d502 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -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 diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 8440a4368e..11f6c35deb 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -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. `^#!.+?$` to skip shebangs). + diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java index ee2ae4fda0..ff89f55ead 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java @@ -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. @@ -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(); @@ -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 { diff --git a/testlib/src/main/resources/license/SkipLines.test b/testlib/src/main/resources/license/SkipLines.test new file mode 100644 index 0000000000..4048868ac6 --- /dev/null +++ b/testlib/src/main/resources/license/SkipLines.test @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/testlib/src/main/resources/license/SkipLinesHasLicense.test b/testlib/src/main/resources/license/SkipLinesHasLicense.test new file mode 100644 index 0000000000..f645c5b2e2 --- /dev/null +++ b/testlib/src/main/resources/license/SkipLinesHasLicense.test @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java index 137937beb9..2b1d0aff07 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -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. @@ -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("", "^(?!