Skip to content

Fun with line endings #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,23 @@ spotless {
// when writing a custom step, it will be helpful to know
// how the formatting process works, which is as follows:

// 1) Load each target file, and convert it to unix-style line endings ('\n')
// 1) Load each target file
// 2) Pass its content through a series of steps, feeding the output of each step to the next
// 3) Put the correct line endings back on, then either check or apply
// 3) Then either check or apply

// each step receives a string as input, and should output
// a formatted string as output. Each step can trust that its
// input will have unix newlines, and it must promise to output
// only unix newlines. Other than that, anything is fair game!
// a formatted string as output.
}

// If you don't specify a format specific line ending, the spotless-global setting will be used.
lineEndings = 'UNIX'
}

// If you'd like to specify that files should always have a certain line ending, you can,
// but the default value of PLATFORM_NATIVE is *highly* recommended
lineEndings = PLATFORM_NATIVE // can be WINDOWS, UNIX, or PLATFORM_NATIVE
// but the default value of PLATFORM_NATIVE is *highly* recommended.
// DERIVED means the line ending is derived for each file based on the first line ending
// in its content. If none is found PLATFORM_NATIVE is used.
lineEndings = 'PLATFORM_NATIVE' // can be WINDOWS, UNIX, PLATFORM_NATIVE or DERIVED
}
```

Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ dependencies {
// UNCOMMENT TO DOWNLOAD SOURCE JARS
//embeddedJars "p2:${name}.source:${ver}"
}

configurations.compile.extendsFrom(configurations.embeddedJars)

testCompile 'org.mockito:mockito-core:1.10.19', {
exclude group:'org.hamcrest', module: 'hamcrest-core'
}
}

jar {
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/com/diffplug/gradle/spotless/FileEndingStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2016 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.gradle.spotless;

public class FileEndingStep {
private LineEnding lineEnding;
private LineEndingService lineEndingService;
private boolean clobber = true;

public FileEndingStep(LineEnding lineEnding) {
this(lineEnding, new LineEndingService());
}

FileEndingStep(LineEnding lineEnding, LineEndingService lineEndingService) {
this.lineEnding = lineEnding;
this.lineEndingService = lineEndingService;
}

public void disableClobber() {
this.clobber = false;
}

public String format(String input) {
return clobber ? formatWithClobber(input) : formatWithoutClobber(input);
}

public String formatWithClobber(String input) {
String lineSeparator = lineEndingService.getLineSeparatorOrDefault(lineEnding, input);
int indexOfLastNonWhitespaceCharacter = lineEndingService.indexOfLastNonWhitespaceCharacter(input);

if (indexOfLastNonWhitespaceCharacter == -1) {
return lineSeparator;
}

StringBuilder builder = new StringBuilder(indexOfLastNonWhitespaceCharacter + 2);
builder.append(input, 0, indexOfLastNonWhitespaceCharacter + 1);
builder.append(lineSeparator);
return builder.toString();
}

public String formatWithoutClobber(String input) {
String lineSeparator = lineEndingService.getLineSeparatorOrDefault(lineEnding, input);
return input.endsWith(lineSeparator) ? input : input + lineSeparator;
}

}
96 changes: 46 additions & 50 deletions src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import org.gradle.api.GradleException;
Expand All @@ -35,20 +36,25 @@ public class FormatExtension {
protected final String name;
protected final SpotlessExtension root;

/** The steps that need to be added. */
protected List<FormatterStep> steps = new ArrayList<>();

/** The files that need to be formatted. */
protected FileCollection target;

private Optional<LineEnding> lineEndings = Optional.empty();

public FormatExtension(String name, SpotlessExtension root) {
this.name = name;
this.root = root;
root.addFormatExtension(this);
}

/** The files that need to be formatted. */
protected FileCollection target;
// Adding LineEndingStep by default in order to be compatible to v1.3.3
customLazy("defaultLineEnding", () -> new LineEndingStep(root.getLineEndings())::format);
}

/**
* FileCollections pass through raw.
* Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir.
* List<String> are treates as the 'includes' arg to fileTree, with project.rootDir as the dir.
* Anything else gets passed to getProject().files().
* FileCollections pass through raw. Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir. List<String> are treates as the 'includes' arg to fileTree, with project.rootDir as the dir. Anything else gets passed to getProject().files().
*/
public void target(Object... targets) {
if (targets.length == 0) {
Expand Down Expand Up @@ -87,13 +93,18 @@ protected FileCollection parseTarget(Object target) {
}
}

/** The steps that need to be added. */
protected List<FormatterStep> steps = new ArrayList<>();
public LineEnding getLineEndings() {
return lineEndings.orElse(root.getLineEndings());
}

public void setLineEndings(LineEnding lineEndings) {
this.lineEndings = Optional.of(lineEndings);
dontDoDefaultLineEndingNormalization();
customLazy("lineEnding", () -> new LineEndingStep(lineEndings)::format);
}

/**
* Adds the given custom step, which is constructed lazily for performance reasons.
*
* The resulting function will receive a string with unix-newlines, and it must return a string unix newlines.
*/
public void customLazy(String name, Throwing.Supplier<Throwing.Function<String, String>> formatterSupplier) {
for (FormatterStep step : steps) {
Expand All @@ -104,12 +115,12 @@ public void customLazy(String name, Throwing.Supplier<Throwing.Function<String,
steps.add(FormatterStep.createLazy(name, formatterSupplier));
}

/** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */
/** Adds a custom step. */
public void custom(String name, Closure<String> formatter) {
custom(name, formatter::call);
}

/** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */
/** Adds a custom step. */
public void custom(String name, Throwing.Function<String, String> formatter) {
customLazy(name, () -> formatter);
}
Expand All @@ -122,7 +133,7 @@ public void customReplace(String name, CharSequence original, CharSequence after
/** Highly efficient find-replace regex. */
public void customReplaceRegex(String name, String regex, String replacement) {
customLazy(name, () -> {
Pattern pattern = Pattern.compile(regex, Pattern.UNIX_LINES | Pattern.MULTILINE);
Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE);
return raw -> pattern.matcher(raw).replaceAll(replacement);
});
}
Expand All @@ -134,36 +145,7 @@ public void trimTrailingWhitespace() {

/** Ensures that files end with a single newline. */
public void endWithNewline() {
custom("endWithNewline", raw -> {
// simplifies the logic below if we can assume length > 0
if (raw.isEmpty()) {
return "\n";
}

// find the last character which has real content
int lastContentCharacter = raw.length() - 1;
char c;
while (lastContentCharacter >= 0) {
c = raw.charAt(lastContentCharacter);
if (c == '\n' || c == '\t' || c == ' ') {
--lastContentCharacter;
} else {
break;
}
}

// if it's already clean, no need to create another string
if (lastContentCharacter == -1) {
return "\n";
} else if (lastContentCharacter == raw.length() - 2 && raw.charAt(raw.length() - 1) == '\n') {
return raw;
} else {
StringBuilder builder = new StringBuilder(lastContentCharacter + 2);
builder.append(raw, 0, lastContentCharacter + 1);
builder.append('\n');
return builder.toString();
}
});
customLazy("endWithNewline", () -> new FileEndingStep(getLineEndings())::format);
}

/** Ensures that the files are indented using spaces. */
Expand All @@ -187,19 +169,23 @@ public void indentWithTabs() {
}

/**
* @param licenseHeader Content that should be at the top of every file
* @param delimiter Spotless will look for a line that starts with this to know what the "top" is.
* @param licenseHeader
* Content that should be at the top of every file
* @param delimiter
* Spotless will look for a line that starts with this to know what the "top" is.
*/
public void licenseHeader(String licenseHeader, String delimiter) {
customLazy(LicenseHeaderStep.NAME, () -> new LicenseHeaderStep(licenseHeader, delimiter)::format);
customLazy(LicenseHeaderStep.NAME, () -> new LicenseHeaderStep(licenseHeader, delimiter, getLineEndings())::format);
}

/**
* @param licenseHeaderFile Content that should be at the top of every file
* @param delimiter Spotless will look for a line that starts with this to know what the "top" is.
* @param licenseHeaderFile
* Content that should be at the top of every file
* @param delimiter
* Spotless will look for a line that starts with this to know what the "top" is.
*/
public void licenseHeaderFile(Object licenseHeaderFile, String delimiter) {
customLazy(LicenseHeaderStep.NAME, () -> new LicenseHeaderStep(getProject().file(licenseHeaderFile), delimiter)::format);
customLazy(LicenseHeaderStep.NAME, () -> new LicenseHeaderStep(getProject().file(licenseHeaderFile), delimiter, getLineEndings())::format);
}

/** Sets up a FormatTask according to the values in this extension. */
Expand All @@ -212,4 +198,14 @@ protected void setupTask(FormatTask task) throws Exception {
protected Project getProject() {
return root.project;
}

// As long defaultLineEnding is active by default, we need to be able to disable the
// eol normalization for the tests.
protected void dontDoDefaultLineEndingNormalization() {
Optional<FormatterStep> lineEndingStep = steps.stream()
.filter(step -> "defaultLineEnding".equals(step.getName()))
.findFirst();

lineEndingStep.ifPresent(steps::remove);
}
}
4 changes: 1 addition & 3 deletions src/main/java/com/diffplug/gradle/spotless/FormatTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public class FormatTask extends DefaultTask {
@Input
public boolean check = false;
@Input
public LineEnding lineEndings = LineEnding.PLATFORM_NATIVE;
@Input
public List<FormatterStep> steps = new ArrayList<>();

@TaskAction
Expand All @@ -44,7 +42,7 @@ public void format() throws Exception {
throw new GradleException("You must specify 'Iterable<File> toFormat'");
}
// combine them into the master formatter
Formatter formatter = new Formatter(lineEndings, getProject().getProjectDir().toPath(), steps);
Formatter formatter = new Formatter(getProject().getProjectDir().toPath(), steps);

// perform the check
if (check) {
Expand Down
40 changes: 9 additions & 31 deletions src/main/java/com/diffplug/gradle/spotless/Formatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,69 +29,47 @@

/** Formatter which performs the full formatting. */
public class Formatter {
private final LineEnding lineEnding;
private final Path projectDirectory;
private final List<FormatterStep> steps;
private final Logger logger = Logging.getLogger(Formatter.class);

public Formatter(LineEnding lineEnding, Path projectDirectory, List<FormatterStep> steps) {
this.lineEnding = lineEnding;
public Formatter(Path projectDirectory, List<FormatterStep> steps) {
this.projectDirectory = projectDirectory;
this.steps = new ArrayList<>(steps);
}

/** Returns true iff the given file's formatting is up-to-date. */
public boolean isClean(File file) throws IOException {
String raw = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
String unix = raw.replaceAll("\r", "");

// check the newlines
int totalNewLines = (int) unix.codePoints().filter(val -> val == '\n').count();
int windowsNewLines = raw.length() - unix.length();
if (lineEnding.isWin()) {
if (windowsNewLines != totalNewLines) {
return false;
}
} else {
if (windowsNewLines != 0) {
return false;
}
}

// check the other formats
String formatted = applyAll(unix, file);
String formatted = applyAll(raw, file);

// return true iff the formatted string equals the unix one
return formatted.equals(unix);
// return true iff the formatted string equals raw
return formatted.equals(raw);
}

/** Applies formatting to the given file. */
public void applyFormat(File file) throws IOException {
String raw = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
String unix = raw.replaceAll("\r", "");

// enforce the format
unix = applyAll(unix, file);

// convert the line endings if necessary
if (!lineEnding.string.equals("\n")) {
unix = unix.replace("\n", lineEnding.string);
}
raw = applyAll(raw, file);

// write out the file
Files.write(file.toPath(), unix.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING);
Files.write(file.toPath(), raw.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING);
}

/** Returns the result of calling all of the FormatterSteps. */
String applyAll(String unix, File file) {
String applyAll(String raw, File file) {
for (FormatterStep step : steps) {
try {
unix = step.format(unix, file);
raw = step.format(raw, file);
} catch (Throwable e) {
logger.warn("Unable to apply step " + step.getName() + " to " + projectDirectory.relativize(file.toPath()) + ": " + e.getMessage());
logger.info("Exception is ", e);
}
}
return unix;
return raw;
}
}
7 changes: 2 additions & 5 deletions src/main/java/com/diffplug/gradle/spotless/FormatterStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@

/**
* An implementation of this class specifies a single step in a formatting process.
*
* The input is guaranteed to have unix-style newlines, and the output is required
* to not introduce any windows-style newlines as well.
*/
public interface FormatterStep {
/** The name of the step, for debugging purposes. */
Expand All @@ -36,9 +33,9 @@ public interface FormatterStep {
/**
* Returns a formatted version of the given content.
*
* @param raw File's content, guaranteed to have unix-style newlines ('\n')
* @param raw File's content
* @param file the File which is being formatted
* @return The formatted content, guaranteed to only have unix-style newlines
* @return The formatted content
* @throws Throwable
*/
String format(String raw, File file) throws Throwable;
Expand Down
Loading