Skip to content
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

RDF Formatter Contribution #2261

Merged
merged 15 commits into from
Sep 27, 2024
Merged
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ 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
* Support for `rdf` ([#2261](https://github.com/diffplug/spotless/pull/2261))
### Changed
* Support configuring the Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238))

* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`.
([#2259](https://github.com/diffplug/spotless/pull/2259))

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}}
lib('pom.SortPomStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('protobuf.BufStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
lib('rdf.RdfFormatterStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('shell.ShfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
Expand Down Expand Up @@ -157,6 +158,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`protobuf.BufStep`](lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
| [`rdf.RdfFormatterStep`](lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
| [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`shell.ShfmtStep`](lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
Expand Down
2 changes: 0 additions & 2 deletions lib/src/main/java/com/diffplug/spotless/Formatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ public boolean isClean(File file) throws IOException {

String raw = new String(Files.readAllBytes(file.toPath()), encoding);
String unix = LineEnding.toUnix(raw);

// check the newlines (we can find these problems without even running the steps)
int totalNewLines = (int) unix.codePoints().filter(val -> val == '\n').count();
int windowsNewLines = raw.length() - unix.length();
Expand All @@ -181,7 +180,6 @@ public boolean isClean(File file) throws IOException {

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

// return true iff the formatted string equals the unix one
return formatted.equals(unix);
}
Expand Down
106 changes: 106 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2024 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.spotless.rdf;

import java.io.Serializable;
import java.util.Objects;

public class RdfFormatterConfig implements Serializable {
private static final long serialVersionUID = 1L;
private boolean failOnWarning = true;
private String turtleFormatterVersion = RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION;
private boolean verify = true;

public RdfFormatterConfig() {}

public void setFailOnWarning(boolean failOnWarning) {
this.failOnWarning = failOnWarning;
}

public boolean isFailOnWarning() {
return failOnWarning;
}

public boolean isVerify() {
return verify;
}

public void setVerify(boolean verify) {
this.verify = verify;
}

public static Builder builder() {
return new Builder();
}

public String getTurtleFormatterVersion() {
return turtleFormatterVersion;
}

public void setTurtleFormatterVersion(String turtleFormatterVersion) {
this.turtleFormatterVersion = turtleFormatterVersion;
}

public static class Builder {
RdfFormatterConfig config = new RdfFormatterConfig();

public Builder() {}

public Builder failOnWarning() {
return this.failOnWarning(true);
}

public Builder failOnWarning(boolean fail) {
this.config.setFailOnWarning(fail);
return this;
}

public Builder turtleFormatterVersion(String version) {
this.config.turtleFormatterVersion = version;
return this;
}

public Builder verify(boolean verify) {
this.config.verify = verify;
return this;
}

public Builder verify() {
this.config.verify = true;
return this;
}

public RdfFormatterConfig build() {
return config;
}
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof RdfFormatterConfig))
return false;
RdfFormatterConfig that = (RdfFormatterConfig) o;
return isFailOnWarning() == that.isFailOnWarning()
&& Objects.equals(turtleFormatterVersion, that.turtleFormatterVersion);
}

@Override
public int hashCode() {
return Objects.hash(isFailOnWarning(), turtleFormatterVersion);
}
}
165 changes: 165 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterFunc.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2024 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.spotless.rdf;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.LineEnding;

public class RdfFormatterFunc implements FormatterFunc {
private static final Set<String> TURTLE_EXTENSIONS = Set.of("ttl", "turtle");
private static final Set<String> TRIG_EXTENSIONS = Set.of("trig");
private static final Set<String> NTRIPLES_EXTENSIONS = Set.of("n-triples", "ntriples", "nt");
private static final Set<String> NQUADS_EXTENSIONS = Set.of("n-quads", "nquads", "nq");

private final RdfFormatterStep.State state;
private final ReflectionHelper reflectionHelper;

public RdfFormatterFunc(RdfFormatterStep.State state)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
this.state = state;
this.reflectionHelper = new ReflectionHelper(state);
}

@Override
public String apply(String input) throws Exception {
throw new UnsupportedOperationException("We need to know the filename so we can guess the RDF format. Use apply(String, File) instead!");
}

@Override
public String apply(String rawUnix, File file) throws Exception {
String filename = file.getName().toLowerCase(Locale.US);
int lastDot = filename.lastIndexOf('.');
if (lastDot < 0) {
throw new IllegalArgumentException(
String.format("File %s has no file extension, cannot determine RDF format", file.getAbsolutePath()));
}
if (lastDot + 1 >= filename.length()) {
throw new IllegalArgumentException(
String.format("File %s has no file extension, cannot determine RDF format", file.getAbsolutePath()));
}
String extension = filename.substring(lastDot + 1);

try {
if (TURTLE_EXTENSIONS.contains(extension)) {
return formatTurtle(rawUnix, file, reflectionHelper);
}
if (TRIG_EXTENSIONS.contains(extension)) {
return formatTrig(rawUnix, file);
}
if (NTRIPLES_EXTENSIONS.contains(extension)) {
return formatNTriples(rawUnix, file);
}
if (NQUADS_EXTENSIONS.contains(extension)) {
return formatNQuads(rawUnix, file);
}
throw new IllegalArgumentException(String.format("Cannot handle file with extension %s", extension));
} catch (InvocationTargetException e) {
throw new RuntimeException("Error formatting file " + file.getPath(), e.getCause());
} catch (Exception e) {
throw new RuntimeException("Error formatting file " + file.getPath(), e);
}
}

private String formatNQuads(String rawUnix, File file) {
throw new UnsupportedOperationException("NQUADS formatting not supported yet");
}

private String formatNTriples(String rawUnix, File file) {
throw new UnsupportedOperationException("NTRIPLES formatting not supported yet");
}

private String formatTrig(String rawUnix, File file) {
throw new UnsupportedOperationException("TRIG formatting not supported yet");
}

private String formatTurtle(String rawUnix, File file, ReflectionHelper reflectionHelper)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException,
NoSuchFieldException, InstantiationException {
String formatted;
Object lang = reflectionHelper.getLang("TTL");
formatted = reflectionHelper.formatWithTurtleFormatter(rawUnix);
if (state.getConfig().isVerify()) {
veryfyResult(rawUnix, file, reflectionHelper, lang, formatted);
}
return LineEnding.toUnix(formatted);
}

private static void veryfyResult(String rawUnix, File file, ReflectionHelper reflectionHelper, Object lang,
String formatted) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object modelBefore = reflectionHelper.parseToModel(rawUnix, file, lang);
Object modelAfter = reflectionHelper.parseToModel(formatted, file, lang);
if (!reflectionHelper.areModelsIsomorphic(modelBefore, modelAfter)) {
long beforeSize = reflectionHelper.modelSize(modelBefore);
long afterSize = reflectionHelper.modelSize(modelAfter);
String diffResult;
if (beforeSize != afterSize) {
diffResult = String.format("< %,d triples", beforeSize);
diffResult += String.format("> %,d triples", afterSize);
} else {
diffResult = calculateDiff(reflectionHelper, modelBefore, modelAfter);
}
throw new IllegalStateException(
"Formatted RDF is not isomorphic with original, which means that formatting changed the data.\n"
+ "This could be a bug in the formatting system leading to data corruption and should be reported. \n"
+ "If you are not scared to lose data, you can disable this check by setting the config option 'verify' to 'false'"
+ "\n\nDiff:\n"
+ diffResult);
}
}

private static String calculateDiff(ReflectionHelper reflectionHelper, Object modelBefore, Object modelAfter)
throws InvocationTargetException, IllegalAccessException {
String diffResult;
Object graphBefore = reflectionHelper.getGraph(modelBefore);
Object graphAfter = reflectionHelper.getGraph(modelAfter);

List<Object> onlyInBeforeContent = reflectionHelper.streamGraph(graphBefore)
.filter(triple -> {
try {
return !reflectionHelper.graphContainsSameTerm(graphAfter, triple);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());

List<Object> onlyInAfterContent = reflectionHelper.streamGraph(graphAfter)
.filter(triple -> {
try {
return !reflectionHelper.graphContainsSameTerm(graphBefore, triple);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
if (!(onlyInBeforeContent.isEmpty() && onlyInAfterContent.isEmpty())) {
diffResult = onlyInBeforeContent.stream().map(s -> String.format("< %s", s))
.collect(Collectors.joining("\n"));
diffResult += "\n" + onlyInAfterContent.stream().map(s -> String.format("> %s", s)).collect(Collectors.joining("\n"));
} else {
diffResult = "'before' and 'after' content differs, but we don't know why. This is probably a bug.";
}
return diffResult;
}

}
Loading
Loading