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 edb4d73f6f..6eeaf8e4f2 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java @@ -59,7 +59,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, null); + return new LicenseHeaderStep(null, null, headerLazy, delimiter, DEFAULT_YEAR_DELIMITER, () -> YearMode.PRESERVE, null, null); } final String name; @@ -67,16 +67,18 @@ public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier head final ThrowingEx.Supplier headerLazy; final String delimiter; final String yearSeparator; + final String yearStrFmt; final Supplier yearMode; final @Nullable String skipLinesMatching; - private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier yearMode, @Nullable String skipLinesMatching) { + private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier yearMode, @Nullable String skipLinesMatching, @Nullable String yearStrFmt) { this.name = sanitizeName(name); 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.yearStrFmt = yearStrFmt; this.skipLinesMatching = sanitizePattern(skipLinesMatching); } @@ -85,11 +87,11 @@ public String getName() { } public LicenseHeaderStep withName(String name) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withContentPattern(String contentPattern) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withHeaderString(String header) { @@ -97,15 +99,15 @@ public LicenseHeaderStep withHeaderString(String header) { } public LicenseHeaderStep withHeaderLazy(ThrowingEx.Supplier headerLazy) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withDelimiter(String delimiter) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withYearSeparator(String yearSeparator) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withYearMode(YearMode yearMode) { @@ -113,11 +115,15 @@ public LicenseHeaderStep withYearMode(YearMode yearMode) { } public LicenseHeaderStep withYearModeLazy(Supplier yearMode) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); + } + + public LicenseHeaderStep withYearStingFormat(String yearStrFmt) { + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } public LicenseHeaderStep withSkipLinesMatching(@Nullable String skipLinesMatching) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching, yearStrFmt); } private static class SetLicenseHeaderYearsFromGitHistory implements SerializedFunction { @@ -134,7 +140,7 @@ public FormatterStep build() { if (yearMode.get() == YearMode.SET_FROM_GIT) { formatterStep = FormatterStep.createLazy(name, () -> { boolean updateYear = false; // doesn't matter - return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching); + return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching, yearStrFmt); }, new SetLicenseHeaderYearsFromGitHistory()); } else { formatterStep = FormatterStep.createLazy(name, () -> { @@ -151,7 +157,7 @@ public FormatterStep build() { default: throw new IllegalStateException(yearMode.toString()); } - return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching); + return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching, yearStrFmt); }, step -> FormatterFunc.needsFile(step::format)); } if (contentPattern == null) { @@ -214,6 +220,7 @@ private static final class Runtime implements Serializable { private final Pattern delimiterPattern; private final @Nullable Pattern skipLinesMatching; + private final String yearStrFormat; private final String yearSepOrFull; private final @Nullable String yearToday; private final @Nullable String beforeYear; @@ -225,7 +232,7 @@ private static final class Runtime implements Serializable { private static final Pattern FILENAME_PATTERN = Pattern.compile("\\$FILE"); /** The license that we'd like enforced. */ - private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest, @Nullable String skipLinesMatching) { + private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest, @Nullable String skipLinesMatching, @Nullable String yearStrFormat) { if (delimiter.contains("\n")) { throw new IllegalArgumentException("The delimiter must not contain any newlines."); } @@ -236,6 +243,7 @@ private Runtime(String licenseHeader, String delimiter, String yearSeparator, bo } this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE); this.skipLinesMatching = skipLinesMatching == null ? null : Pattern.compile(skipLinesMatching); + this.yearStrFormat = yearStrFormat == null ? "%s" : yearStrFormat; this.hasFileToken = FILENAME_PATTERN.matcher(licenseHeader).find(); Optional yearToken = getYearToken(licenseHeader); @@ -303,11 +311,20 @@ private String format(String raw, File file) { } private String addOrUpdateLicenseHeader(String raw, File file) { + System.out.println("YEARFMT DEBUG: addOrUpdateLicenseHeader() start"); + System.out.println("YEARFMT DEBUG: addOrUpdateLicenseHeader() raw: " + raw); + System.out.println("YEARFMT DEBUG: addOrUpdateLicenseHeader() file: " + file.getAbsolutePath()); raw = replaceYear(raw); return replaceFileName(raw, file); } + private String formatYearStr(String year) { + return yearStrFormat.formatted(year); + } + private String replaceYear(String raw) { + System.out.println("YEARFMT DEBUG: replaceYear() start"); + System.out.println("YEARFMT DEBUG: replaceYear() raw: " + raw); Matcher contentMatcher = delimiterPattern.matcher(raw); if (!contentMatcher.find()) { throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern); @@ -318,8 +335,15 @@ private String replaceYear(String raw) { if (contentMatcher.start() == yearSepOrFull.length() && raw.startsWith(yearSepOrFull)) { // if no change is required, return the raw string without // creating any other new strings for maximum performance + System.out.println("YEARFMT DEBUG: 0: raw: '" + raw + "'"); return raw; } else { + final String yearStr = formatYearStr(yearSepOrFull); + + System.out.println("YEARFMT DEBUG: 1: yearSepOrFull: '" + yearSepOrFull + "'"); + System.out.println("YEARFMT DEBUG: 1: yearStr: '" + yearStr + "'"); + + // return yearStr + content; // otherwise we'll have to add the header return yearSepOrFull + content; } @@ -339,11 +363,27 @@ private String replaceYear(String raw) { return raw; } } + + // final String yearStr = formatYear(newYear); + System.out.println("YEARFMT DEBUG: 2: beforeYear: '" + beforeYear + "'"); + System.out.println("YEARFMT DEBUG: 2: newYear: '" + newYear + "'"); + System.out.println("YEARFMT DEBUG: 2: afterYear: '" + afterYear + "'"); + // System.out.println("YEARFMT DEBUG: 2: yearStr: '" + yearStr + "'"); + + // return beforeYear + yearStr + afterYear + content; return beforeYear + newYear + afterYear + content; } else { String newYear = calculateYearBySearching(raw.substring(0, contentMatcher.start())); + final String yearStr = formatYearStr(newYear); + + System.out.println("YEARFMT DEBUG: 3: beforeYear: '" + beforeYear + "'"); + System.out.println("YEARFMT DEBUG: 3: newYear: '" + newYear + "'"); + System.out.println("YEARFMT DEBUG: 3: afterYear: '" + afterYear + "'"); + System.out.println("YEARFMT DEBUG: 3: yearStr: '" + yearStr + "'"); + // at worst, we just say that it was made today - return beforeYear + newYear + afterYear + content; + return beforeYear + yearStr + afterYear + content; + // return beforeYear + newYear + afterYear + content; } } } @@ -353,26 +393,30 @@ private String replaceYear(String raw) { /** Calculates the year to inject. */ private String calculateYearExact(String parsedYear) { - if (parsedYear.equals(yearToday)) { - return parsedYear; + System.out.println("YEARFMT DEBUG: calculateYearExact() start"); + System.out.println("YEARFMT DEBUG: calculateYearExact() parsedYear: " + parsedYear); + if (parsedYear.equals(formatYearStr(yearToday))) { + return formatYearStr(parsedYear); } else if (YYYY.matcher(parsedYear).matches()) { if (updateYearWithLatest) { if (licenseHeaderWithRange) { - return yearToday; + return formatYearStr(yearToday); } else { - return parsedYear + yearSepOrFull + yearToday; + return formatYearStr(parsedYear + yearSepOrFull + yearToday); } } else { // it's already good as a single year - return parsedYear; + return formatYearStr(parsedYear); } } else { - return calculateYearBySearching(parsedYear); + return formatYearStr(calculateYearBySearching(parsedYear)); } } /** Searches the given string for YYYY, and uses that to determine the year range. */ private String calculateYearBySearching(String content) { + System.out.println("YEARFMT DEBUG: calculateYearBySearching() start"); + System.out.println("YEARFMT DEBUG: calculateYearBySearching() content: " + content); Matcher yearMatcher = YYYY.matcher(content); if (yearMatcher.find()) { String firstYear = yearMatcher.group(); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 03d3f6d8b7..61370a40fe 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -7,6 +7,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added - Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960)) - `flexmark()` step now supports arbitrary formatter options via the `formatterOptions` map. ([#2968](https://github.com/diffplug/spotless/pull/2968)) +- Add support for custom string format for license header copyright year via `yearStringFormat()`. ([#2965](https://github.com/diffplug/spotless/pull/2965)) ### Fixed - `toggleOffOn` no longer disables lint-only steps such as `forbidWildcardImports`. ([#2962](https://github.com/diffplug/spotless/pull/2962)) - Prevent build caches from interfering when executing under the `-PspotlessIdeHook` mode. ([#2365](https://github.com/diffplug/spotless/issues/2365)) 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 e17f37b0fa..48f3e50eb9 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 @@ -614,6 +614,15 @@ public LicenseHeaderConfig yearSeparator(String yearSeparator) { return this; } + /** + * @param yearStrFmt The String format used to format the year part of the license header. + */ + public LicenseHeaderConfig yearStringFormat(String yearStrFmt) { + builder = builder.withYearStingFormat(yearStrFmt); + replaceStep(createStep()); + return this; + } + public LicenseHeaderConfig skipLinesMatching(String skipLinesMatching) { builder = builder.withSkipLinesMatching(skipLinesMatching); replaceStep(createStep()); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java index a8ac2dd9cc..bfd17595ea 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,26 +43,39 @@ private void setLicenseStep(String licenseLine) throws IOException { "}"); } - private void assertUnchanged(String year) throws IOException { - assertTransform(year, year); + private String formatYearStr(String yearFmt, String year) { + if (yearFmt == null) { + yearFmt = "%s"; + } + return yearFmt.formatted(year); + } + + private void assertUnchanged(String yearFmt, String year) throws IOException { + assertTransform(yearFmt, year, year); } - private void assertTransform(String yearBefore, String yearAfter) throws IOException { + private void assertTransform(String yearFmt, String yearBefore, String yearAfter) throws IOException { + final String yearAfterStr = formatYearStr(yearFmt, yearAfter); + setFile(TEST_JAVA).toContent("/** " + yearBefore + " */\n" + CONTENT); gradleRunner().withArguments("spotlessApply", "--stacktrace").forwardOutput().build(); - assertFile(TEST_JAVA).hasContent("/** " + yearAfter + " */\n" + CONTENT); + assertFile(TEST_JAVA).hasContent("/** " + yearAfterStr + " */\n" + CONTENT); } private void testSuiteUpdateWithLatest(boolean update) throws IOException { + testSuiteUpdateWithLatest(update, null); + } + + private void testSuiteUpdateWithLatest(boolean update, String yearFmt) throws IOException { if (update) { - assertTransform("2003", "2003-" + NOW); - assertTransform("2003-2005", "2003-" + NOW); + assertTransform(yearFmt, "2003", "2003-" + NOW); + assertTransform(yearFmt, "2003-2005", "2003-" + NOW); } else { - assertUnchanged("2003"); - assertUnchanged("2003-2005"); + assertUnchanged(yearFmt, "2003"); + assertUnchanged(yearFmt, "2003-2005"); } - assertUnchanged(NOW); - assertTransform("", NOW); + assertUnchanged(yearFmt, NOW); + assertTransform(yearFmt, "", NOW); } @Test @@ -71,12 +84,48 @@ void normal() throws IOException { testSuiteUpdateWithLatest(false); } + @Test + void withYearStringFormat_defaultFormat() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').yearStringFormat('%s')"); + testSuiteUpdateWithLatest(false, "%s"); + } + + @Test + void withYearStringFormat_spacesBefore() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').yearStringFormat('%12s')"); + testSuiteUpdateWithLatest(false, "%12s"); + } + + @Test + void withYearStringFormat_spacesAfter() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').yearStringFormat('%-12s')"); + testSuiteUpdateWithLatest(false, "%-12s"); + } + @Test void updateYearWithLatestTrue() throws IOException { setLicenseStep("licenseHeader('/** $YEAR */').updateYearWithLatest(true)"); testSuiteUpdateWithLatest(true); } + @Test + void updateYearWithLatestTrueAndWithYearStringFormat_defaultFormat() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').updateYearWithLatest(true).yearStringFormat('%s')"); + testSuiteUpdateWithLatest(true, "%s"); + } + + @Test + void updateYearWithLatestTrueAndWithYearStringFormat_spacesAfter() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').updateYearWithLatest(true).yearStringFormat('%-15s')"); + testSuiteUpdateWithLatest(true, "%-15s"); + } + + @Test + void updateYearWithLatestTrueAndWithYearStringFormat_spacesBefore() throws IOException { + setLicenseStep("licenseHeader('/** $YEAR */').updateYearWithLatest(true).yearStringFormat('%12s')"); + testSuiteUpdateWithLatest(true, "%12s"); + } + @Test void filterByContentPatternTest() throws IOException { setLicenseStep("licenseHeader('/** $YEAR */').onlyIfContentMatches('.+Test.+').updateYearWithLatest(true)"); 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 ff89f55ead..617dfe328a 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-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,9 @@ public class LicenseHeader implements FormatterStepFactory { @Parameter private String skipLinesMatching; + @Parameter + private String yearStrFmt; + @Override public final FormatterStep newFormatterStep(FormatterStepConfig config) { String delimiterString = delimiter != null ? delimiter : config.getLicenseHeaderDelimiter(); @@ -57,6 +60,7 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) { return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString) .withYearMode(yearMode) .withSkipLinesMatching(skipLinesMatching) + .withYearStingFormat(yearStrFmt) .build() .filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter()); } else {