diff --git a/build.gradle b/build.gradle index ee9589d9b9..1128db6f8a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,6 @@ +import com.diffplug.gradle.spotless.SpotlessTask +import lfformat.LfFormatStep + buildscript { repositories { mavenCentral() @@ -12,13 +15,14 @@ plugins { id "com.github.johnrengelman.shadow" version "${shadowJarVersion}" id 'java' id 'jacoco' + id "com.diffplug.spotless" version "6.11.0" } subprojects { repositories { mavenCentral() } - + apply plugin: 'kotlin' compileKotlin { destinationDir = compileJava.destinationDir @@ -74,3 +78,38 @@ gradle.projectsEvaluated { } } } + + +spotless { + repositories { + mavenCentral() + } + // optional: limit format enforcement to just the files changed by this feature branch + ratchetFrom 'origin/master' + + format 'misc', { + // define the files to apply `misc` to + target '*.gradle', '*.md', '.gitignore' + + // define the steps to apply to those files + trimTrailingWhitespace() + indentWithSpaces() // or spaces. Takes an integer argument if you don't like 4 + endWithNewline() + } + + format 'linguaFranca', { + addStep(LfFormatStep.create()) + target 'test/*/src/**/*.lf' // you have to set the target manually + targetExclude 'test/**/failing/**' + } + + java { + target 'org.lflang*/src/**/*.java', 'buildSrc/**/*.java' + // The following is quoted from https://github.com/google/google-java-format + // "Note: There is no configurability as to the formatter's algorithm for formatting. + // This is a deliberate design decision to unify our code formatting on a single format." + googleJavaFormat('1.15.0').aosp().reflowLongStrings() + formatAnnotations() + } +} +tasks.withType(SpotlessTask) { it.dependsOn(":org.lflang.cli:jarLff") } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000000..d12ffe04e0 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,9 @@ +repositories { + mavenCentral() + dependencies { + // https://mvnrepository.com/artifact/com.diffplug.spotless/spotless-lib + implementation group: 'com.diffplug.spotless', name: 'spotless-lib', version: '2.30.0' + // https://mvnrepository.com/artifact/com.diffplug.spotless/spotless-lib-extra + implementation group: 'com.diffplug.spotless', name: 'spotless-lib-extra', version: '2.30.0' + } +} diff --git a/buildSrc/src/main/java/lfformat/LfFormatStep.java b/buildSrc/src/main/java/lfformat/LfFormatStep.java new file mode 100644 index 0000000000..791c3ae109 --- /dev/null +++ b/buildSrc/src/main/java/lfformat/LfFormatStep.java @@ -0,0 +1,58 @@ +package lfformat; + +import com.diffplug.spotless.FormatterStep; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.List; + +public final class LfFormatStep { + private LfFormatStep() {} + + public static FormatterStep create() { + return new Step(); + } + + private static final class Step implements FormatterStep { + @Override + public String format(@SuppressWarnings("NullableProblems") String rawUnix, File file) + throws IOException, InterruptedException { + // It looks stupid to invoke Java from Java, but it is necessary in + // order to break the circularity of needing the program to be built + // in order for it to be built. + Process p = + new ProcessBuilder( + List.of( + "java", + "-jar", + Path.of( + "org.lflang.cli", + "build", + "libs", + "org.lflang.cli-0.3.1-SNAPSHOT-lff.jar") + .toString(), + "--dry-run", + file.getAbsoluteFile().toString())) + .start(); + StringBuilder output = new StringBuilder(); + BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + if (!output.isEmpty()) output.append("\n"); + // Filtering by "lff: " is yet another string-processing hack that is not airtight! + if (!line.startsWith("lff: ")) output.append(line); + } + int returnCode = p.waitFor(); + if (returnCode != 0) throw new RuntimeException("Failed to reformat file."); + return output.toString().stripTrailing() + "\n"; // Not sure why this is necessary + } + + @SuppressWarnings("NullableProblems") + @Override + public String getName() { + return "Lingua Franca formatting step"; + } + } +} diff --git a/gradle.properties b/gradle.properties index 043d6dc457..596f2987aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,7 @@ version=0.3.1-SNAPSHOT [versions] commonsCliVersion=1.4 +googleJavaFormatVersion=1.8 guiceVersion=5.1.0 jacocoVersion=0.8.7 jupiterVersion=5.8.2 diff --git a/org.lflang.cli/src/org/lflang/cli/Lff.java b/org.lflang.cli/src/org/lflang/cli/Lff.java index dc3234f432..8922a341da 100644 --- a/org.lflang.cli/src/org/lflang/cli/Lff.java +++ b/org.lflang.cli/src/org/lflang/cli/Lff.java @@ -51,6 +51,13 @@ public class Lff extends CliBase { */ enum CLIOption { HELP("h", "help", false, false, "Display this information."), + DRY_RUN( + "d", + "dry-run", + false, + false, + "Send the formatted file contents to stdout without writing to the file system." + ), LINE_WRAP( "w", "wrap", @@ -199,6 +206,8 @@ private void runFormatter(List files) { FormattingUtils.DEFAULT_LINE_LENGTH : Integer.parseInt(cmd.getOptionValue(CLIOption.LINE_WRAP.option.getOpt())); + final boolean dryRun = cmd.hasOption(CLIOption.DRY_RUN.option.getOpt()); + for (Path path : files) { if (cmd.hasOption(CLIOption.VERBOSE.option.getOpt())) { reporter.printInfo("Formatting " + path + ":"); @@ -207,12 +216,17 @@ private void runFormatter(List files) { if ( Files.isDirectory(path)&& !cmd.hasOption(CLIOption.NO_RECURSE.option.getLongOpt()) ) { - formatRecursive(Paths.get("."), path, outputRoot, lineLength); + formatRecursive(Paths.get("."), path, outputRoot, lineLength, dryRun); } else { if (outputRoot == null) { - formatSingleFile(path, path, lineLength); + formatSingleFile(path, path, lineLength, dryRun); } else { - formatSingleFile(path, outputRoot.resolve(path.getFileName()), lineLength); + formatSingleFile( + path, + outputRoot.resolve(path.getFileName()), + lineLength, + dryRun + ); } } } @@ -226,18 +240,24 @@ private void runFormatter(List files) { * @param outputRoot Root output directory. * @param lineLength The preferred maximum number of columns per line. */ - private void formatRecursive(Path curPath, Path inputRoot, Path outputRoot, int lineLength) { + private void formatRecursive( + Path curPath, + Path inputRoot, + Path outputRoot, + int lineLength, + boolean dryRun + ) { Path curDir = inputRoot.resolve(curPath); try (var dirStream = Files.newDirectoryStream(curDir)) { for (Path path : dirStream) { Path newPath = curPath.resolve(path.getFileName()); if (Files.isDirectory(path)) { - formatRecursive(newPath, inputRoot, outputRoot, lineLength); + formatRecursive(newPath, inputRoot, outputRoot, lineLength, dryRun); } else { if (outputRoot == null) { - formatSingleFile(path, path, lineLength); + formatSingleFile(path, path, lineLength, dryRun); } else { - formatSingleFile(path, outputRoot.resolve(newPath), lineLength); + formatSingleFile(path, outputRoot.resolve(newPath), lineLength, dryRun); } } } @@ -249,7 +269,7 @@ private void formatRecursive(Path curPath, Path inputRoot, Path outputRoot, int /** * Load and validate a single file, then format it and output to the given outputPath. */ - private void formatSingleFile(Path file, Path outputPath, int lineLength) { + private void formatSingleFile(Path file, Path outputPath, int lineLength, boolean dryRun) { file = file.normalize(); outputPath = outputPath.normalize(); final Resource resource = getResource(file); @@ -262,24 +282,29 @@ private void formatSingleFile(Path file, Path outputPath, int lineLength) { validateResource(resource); exitIfCollectedErrors(); + final String formattedFileContents = FormattingUtils.render( + resource.getContents().get(0), + lineLength + ); - try { - FileUtil.writeToFile( - FormattingUtils.render(resource.getContents().get(0), lineLength), - outputPath, - true - ); - } catch (IOException e) { - if (e instanceof FileAlreadyExistsException) { - // only happens if a subdirectory is named with ".lf" at the end + if (dryRun) { + System.out.println(formattedFileContents); + } else { + try { + FileUtil.writeToFile(formattedFileContents, outputPath, true); + } catch (IOException e) { + if (e instanceof FileAlreadyExistsException) { + // only happens if a subdirectory is named with ".lf" at the end + reporter.printFatalErrorAndExit( + "Error writing to " + outputPath + + ": file already exists. Make sure that no " + + "file or directory within provided input paths have the same relative " + + "paths."); + } reporter.printFatalErrorAndExit( - "Error writing to " + outputPath + ": file already exists. Make sure that no " - + "file or directory within provided input paths have the same relative " - + "paths."); + "Error writing to " + outputPath + ": " + e.getMessage() + ); } - reporter.printFatalErrorAndExit( - "Error writing to " + outputPath + ": " + e.getMessage() - ); } exitIfCollectedErrors();