diff --git a/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java b/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java index a4de51b90dd..705837dbda0 100644 --- a/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java +++ b/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java @@ -13,6 +13,7 @@ import java.util.Deque; import java.util.List; +import org.apache.commons.lang3.tuple.Pair; import spoon.OutputType; import spoon.SpoonException; import spoon.compiler.Environment; @@ -35,6 +36,7 @@ import spoon.support.sniper.internal.CollectionSourceFragment; import spoon.support.sniper.internal.ElementPrinterEvent; import spoon.support.sniper.internal.ElementSourceFragment; +import spoon.support.sniper.internal.IndentationDetector; import spoon.support.sniper.internal.ModificationStatus; import spoon.support.sniper.internal.MutableTokenWriter; import spoon.support.sniper.internal.PrinterEvent; @@ -132,6 +134,12 @@ public void calculate(CtCompilationUnit compilationUnit, List> types) //use line separator of origin source file setLineSeparator(detectLineSeparator(compilationUnit.getOriginalSourceCode())); + + // use indentation style of origin source file for new elements + Pair indentationInfo = IndentationDetector.detectIndentation(compilationUnit); + mutableTokenWriter.setOriginSourceTabulationSize(indentationInfo.getLeft()); + mutableTokenWriter.setOriginSourceUsesTabulations(indentationInfo.getRight()); + runInContext(new SourceFragmentContextList(mutableTokenWriter, compilationUnit, Collections.singletonList(compilationUnit.getOriginalSourceFragment()), diff --git a/src/main/java/spoon/support/sniper/internal/IndentationDetector.java b/src/main/java/spoon/support/sniper/internal/IndentationDetector.java new file mode 100644 index 00000000000..93cd0696507 --- /dev/null +++ b/src/main/java/spoon/support/sniper/internal/IndentationDetector.java @@ -0,0 +1,95 @@ +/** + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2019 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.sniper.internal; + +import org.apache.commons.lang3.tuple.Pair; +import spoon.reflect.declaration.CtCompilationUnit; +import spoon.reflect.path.CtRole; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility class for detecting the indentation style used in a compilation unit. + */ +public class IndentationDetector { + + private IndentationDetector() { + } + + /** + * Detect the indentation style of the given compilation unit as 1, 2 or 4 spaces or tabs by + * inspecting the whitespace preceding type members of top-level type declarations. + * + * @param cu A compilation unit. + * @return A pair on the form (indentationSize, isTabs) + */ + public static Pair detectIndentation(CtCompilationUnit cu) { + List typeFragments = cu.getOriginalSourceFragment() + .getGroupedChildrenFragments().stream() + .filter(fragment -> fragment instanceof CollectionSourceFragment) + .flatMap(fragment -> extractTypeFragments((CollectionSourceFragment) fragment).stream()) + .collect(Collectors.toList()); + return detectIndentation(typeFragments); + } + + private static Pair detectIndentation(List topLevelTypeFragments) { + List wsPrecedingTypeMembers = new ArrayList<>(); + + for (ElementSourceFragment typeSource : topLevelTypeFragments) { + assert typeSource.getRoleInParent() == CtRole.DECLARED_TYPE; + + List children = typeSource.getChildrenFragments(); + for (int i = 0; i < children.size() - 1; i++) { + if (children.get(i) instanceof TokenSourceFragment + && children.get(i + 1) instanceof ElementSourceFragment) { + + TokenSourceFragment cur = (TokenSourceFragment) children.get(i); + ElementSourceFragment next = (ElementSourceFragment) children.get(i + 1); + if (cur.getType() == TokenType.SPACE && next.getRoleInParent() == CtRole.TYPE_MEMBER) { + wsPrecedingTypeMembers.add(cur.getSourceCode().replace("\n", "")); + } + } + } + } + + return guessIndentationStyle(wsPrecedingTypeMembers); + } + + private static Pair guessIndentationStyle(List wsPrecedingTypeMembers) { + double avgIndent = wsPrecedingTypeMembers.stream() + .map(String::length) + .map(Double::valueOf) + .reduce((acc, next) -> (acc + next) / 2).orElse(1d); + + double diff1 = Math.abs(1d - avgIndent); + double diff2 = Math.abs(2d - avgIndent); + double diff4 = Math.abs(4d - avgIndent); + + int indentationSize; + if (diff1 > diff2) { + indentationSize = diff2 > diff4 ? 4 : 2; + } else { + indentationSize = 1; + } + + boolean usesTabs = wsPrecedingTypeMembers.stream() + .filter(s -> s.contains("\t")) + .count() >= wsPrecedingTypeMembers.size() / 2; + return Pair.of(indentationSize, usesTabs); + } + + private static List extractTypeFragments(CollectionSourceFragment collection) { + return collection.getItems().stream() + .filter(fragment -> fragment instanceof ElementSourceFragment) + .map(fragment -> (ElementSourceFragment) fragment) + .filter(fragment -> fragment.getRoleInParent() == CtRole.DECLARED_TYPE) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/spoon/support/sniper/internal/MutableTokenWriter.java b/src/main/java/spoon/support/sniper/internal/MutableTokenWriter.java index 6241bd8ccde..859fad169f8 100644 --- a/src/main/java/spoon/support/sniper/internal/MutableTokenWriter.java +++ b/src/main/java/spoon/support/sniper/internal/MutableTokenWriter.java @@ -22,8 +22,38 @@ public class MutableTokenWriter implements TokenWriter { private final TokenWriter delegate; private boolean muted = false; + // indentation style to use for new elements + private boolean originSourceUsesTabulations; + private int originSourceTabulationSize; + public MutableTokenWriter(Environment env) { - this.delegate = new DefaultTokenWriter(new PrinterHelper(env)); + this.delegate = new DefaultTokenWriter(new SniperPrinterHelper(env)); + originSourceUsesTabulations = true; + originSourceTabulationSize = 1; + } + + private class SniperPrinterHelper extends PrinterHelper { + private final Environment env; + + SniperPrinterHelper(Environment env) { + super(env); + this.env = env; + } + + /** + * We override this method to use the correct style of indentation for new elements. + */ + @Override + protected void autoWriteTabs() { + int setTabulationSize = env.getTabulationSize(); + env.useTabulations(originSourceUsesTabulations); + env.setTabulationSize(originSourceTabulationSize); + + super.autoWriteTabs(); + + env.setTabulationSize(setTabulationSize); + env.useTabulations(true); + } } /** @@ -40,6 +70,20 @@ public void setMuted(boolean muted) { this.muted = muted; } + /** + * @param originSourceUsesTabulations whether or not the origin source uses tabs for indentation. + */ + public void setOriginSourceUsesTabulations(boolean originSourceUsesTabulations) { + this.originSourceUsesTabulations = originSourceUsesTabulations; + } + + /** + * @param originSourceTabulationSize the amount of indentation used in the origin source. + */ + public void setOriginSourceTabulationSize(int originSourceTabulationSize) { + this.originSourceTabulationSize = originSourceTabulationSize; + } + @Override public TokenWriter writeSeparator(String token) { if (isMuted()) { diff --git a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java index 99ea0c45def..99f906010ae 100644 --- a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java +++ b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java @@ -14,6 +14,7 @@ import spoon.SpoonException; import spoon.refactoring.Refactoring; import spoon.reflect.CtModel; +import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtCodeSnippetExpression; import spoon.reflect.code.CtExpression; @@ -53,6 +54,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -415,6 +417,49 @@ public void testAddedImportStatementPlacedOnSeparateLineInFileWithPackageStateme testSniper("visibility.YamlRepresenter", addArrayListImport, assertImportsPrintedCorrectly); } + @Test + public void testAddedElementsIndentedWithAppropriateIndentationStyle() { + // contract: added elements in a source file should be indented with the same style of + // indentation as the rest of the file + + Consumer> addElements = type -> { + Factory fact = type.getFactory(); + fact.createField(type, new HashSet<>(), fact.Type().INTEGER_PRIMITIVE, "z", fact.createLiteral(3)); + type.getMethod("sum").getBody() + .addStatement(0, fact.createCodeSnippetStatement("System.out.println(z);")); + }; + BiConsumer, String> assertTabs = (type, result) -> { + assertThat(result, containsString("\n\tint z = 3;")); + assertThat(result, containsString("\n\t\tSystem")); + }; + BiConsumer, String> assertTwoSpaces = (type, result) -> { + assertThat(result, containsString("\n int z = 3;")); + assertThat(result, containsString("\n System")); + }; + BiConsumer, String> assertFourSpaces = (type, result) -> { + assertThat(result, containsString("\n int z = 3;")); + assertThat(result, containsString("\n System")); + }; + + testSniper("indentation.Tabs", addElements, assertTabs); + testSniper("indentation.TwoSpaces", addElements, assertTwoSpaces); + testSniper("indentation.FourSpaces", addElements, assertFourSpaces); + } + + @Test + public void testDefaultsToSingleTabIndentationWhenThereAreNoTypeMembers() { + // contract: if there are no type members in a compilation unit, the sniper printer defaults + // to indenting with 1 tab + + Consumer> addField = type -> { + Factory fact = type.getFactory(); + fact.createField(type, new HashSet<>(), fact.Type().INTEGER_PRIMITIVE, "z", fact.createLiteral(3)); + }; + testSniper("indentation.NoTypeMembers", addField, (type, result) -> { + assertThat(result, containsString("\n\tint z = 3;")); + }); + } + /** * 1) Runs spoon using sniper mode, * 2) runs `typeChanger` to modify the code, diff --git a/src/test/resources/indentation/FourSpaces.java b/src/test/resources/indentation/FourSpaces.java new file mode 100644 index 00000000000..933b7f62609 --- /dev/null +++ b/src/test/resources/indentation/FourSpaces.java @@ -0,0 +1,10 @@ +package indentation; + +public class FourSpaces { + private int x = 1; + private int y = 2; + + public int sum() { + return x + y; + } +} diff --git a/src/test/resources/indentation/NoTypeMembers.java b/src/test/resources/indentation/NoTypeMembers.java new file mode 100644 index 00000000000..e434feda3a3 --- /dev/null +++ b/src/test/resources/indentation/NoTypeMembers.java @@ -0,0 +1,4 @@ +package indentation; + +public class NoTypeMembers { +} \ No newline at end of file diff --git a/src/test/resources/indentation/Tabs.java b/src/test/resources/indentation/Tabs.java new file mode 100644 index 00000000000..b088ff1fe38 --- /dev/null +++ b/src/test/resources/indentation/Tabs.java @@ -0,0 +1,10 @@ +package indentation; + +public class Tabs { + private int x = 1; + private int y = 2; + + public int sum() { + return x + y; + } +} \ No newline at end of file diff --git a/src/test/resources/indentation/TwoSpaces.java b/src/test/resources/indentation/TwoSpaces.java new file mode 100644 index 00000000000..13496106033 --- /dev/null +++ b/src/test/resources/indentation/TwoSpaces.java @@ -0,0 +1,10 @@ +package indentation; + +public class TwoSpaces { + private int x = 1; + private int y = 2; + + public int sum() { + return x + y; + } +} \ No newline at end of file