From 8827ec748f075bf45e21c681bf350dcdcdd2dd57 Mon Sep 17 00:00:00 2001 From: maria77102 <121297022+maria77102@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:12:58 +0100 Subject: [PATCH] Automating Formatting of Rune Code (#859) * Created formatter interface * Implement interface + first Test * Fix formatting test + implementation * Enhanced test suite * Implement suggested changes * Improved interface methods/exception handling and added logging * Added formatting tool method * Add dependencies for logging in tools project * Improve implementation of command line tool * Formatting java code * Made formatting happen in-memory * Update logs when resource saved * Fixed logger class name --- .../rosetta/RosettaRuntimeModule.xtend | 6 ++ .../formatting2/ResourceFormatterService.java | 70 ++++++++++++++++ .../formatting2/XtextResourceFormatter.java | 76 ++++++++++++++++++ .../ResourceFormatterServiceTest.java | 75 +++++++++++++++++ .../expected/typeAlias.rosetta | 3 + .../typeAliasWithDocumentation.rosetta | 7 ++ .../formatting-test/input/typeAlias.rosetta | 7 ++ .../input/typeAliasWithDocumentation.rosetta | 8 ++ rosetta-tools/pom.xml | 8 ++ .../rosetta/tools/ResourceFormattingTool.java | 80 +++++++++++++++++++ 10 files changed, 340 insertions(+) create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java create mode 100644 rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java create mode 100644 rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java create mode 100644 rosetta-testing/src/test/resources/formatting-test/expected/typeAlias.rosetta create mode 100644 rosetta-testing/src/test/resources/formatting-test/expected/typeAliasWithDocumentation.rosetta create mode 100644 rosetta-testing/src/test/resources/formatting-test/input/typeAlias.rosetta create mode 100644 rosetta-testing/src/test/resources/formatting-test/input/typeAliasWithDocumentation.rosetta create mode 100644 rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaRuntimeModule.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaRuntimeModule.xtend index d2dbce4d9..a6de2036a 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaRuntimeModule.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/RosettaRuntimeModule.xtend @@ -47,6 +47,8 @@ import com.regnosys.rosetta.config.RosettaConfiguration import com.regnosys.rosetta.config.file.FileBasedRosettaConfigurationProvider import com.regnosys.rosetta.cache.IRequestScopedCache import com.regnosys.rosetta.cache.RequestScopedCache +import com.regnosys.rosetta.formatting2.XtextResourceFormatter +import com.regnosys.rosetta.formatting2.ResourceFormatterService /* Use this class to register components to be used at runtime / without the Equinox extension registry.*/ class RosettaRuntimeModule extends AbstractRosettaRuntimeModule { @@ -148,4 +150,8 @@ class RosettaRuntimeModule extends AbstractRosettaRuntimeModule { def Class bindIRequestScopedCache() { RequestScopedCache } + + def Class bindResourceFormatterService() { + XtextResourceFormatter + } } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java new file mode 100644 index 000000000..fd6633330 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java @@ -0,0 +1,70 @@ +package com.regnosys.rosetta.formatting2; + +import java.util.Collection; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.preferences.ITypedPreferenceValues; +import org.eclipse.xtext.resource.XtextResource; + +public interface ResourceFormatterService { + + /** + * Formats each {@link XtextResource} in the provided collection in-memory. + *

+ * This method iterates over the given collection of resources and applies formatting + * directly to each resource. Formatting may include indentation, spacing adjustments, + * and other stylistic improvements to ensure consistency and readability of the resources. + *

+ * + * @param resources a collection of {@link XtextResource} objects to be formatted + */ + default void formatCollection(Collection resources) { + formatCollection(resources, null); + } + + /** + * Formats the given {@link XtextResource} in-memory. + *

+ * This method applies formatting directly to the specified resource. Formatting can include + * adjustments to indentation, spacing, and other stylistic elements to ensure consistency + * and readability of the resource content. + *

+ * + * @param resources the {@link XtextResource} to format + * @param preferenceValues an {@link ITypedPreferenceValues} object containing formatting preferences, + * or {@code null} if no preferences are specified + */ + default void formatXtextResource(XtextResource resource) { + formatXtextResource(resource, null); + } + + /** + * Formats each {@link XtextResource} in the provided collection in-memory, with specified formatting preferences. + *

+ * This method iterates over the given collection of resources and applies formatting + * directly to each resource. Formatting may include indentation, spacing adjustments, + * and other stylistic improvements to ensure consistency and readability of the resources. + * The formatting can be customized based on the specified {@link ITypedPreferenceValues}. + * If no preferences are required, {@code preferenceValues} can be set to {@code null}. + *

+ * + * @param resources a collection of {@link XtextResource} objects to be formatted + * @param preferenceValues an {@link ITypedPreferenceValues} object containing formatting preferences, + * or {@code null} if no preferences are specified + */ + void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues); + + /** + * Formats the given {@link XtextResource} in-memory. + *

+ * This method applies formatting directly to the specified resource. Formatting can include + * adjustments to indentation, spacing, and other stylistic elements to ensure consistency + * and readability of the resource content. + * The formatting can be customized based on the specified {@link ITypedPreferenceValues}. + * If no preferences are required, {@code preferenceValues} can be set to {@code null}. + *

+ * + * @param resource the {@link XtextResource} to format + */ + void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues); +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java new file mode 100644 index 000000000..1027d86a6 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java @@ -0,0 +1,76 @@ +package com.regnosys.rosetta.formatting2; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.formatting2.FormatterRequest; +import org.eclipse.xtext.formatting2.IFormatter2; +import org.eclipse.xtext.formatting2.regionaccess.ITextRegionAccess; +import org.eclipse.xtext.formatting2.regionaccess.ITextRegionRewriter; +import org.eclipse.xtext.formatting2.regionaccess.ITextReplacement; +import org.eclipse.xtext.formatting2.regionaccess.TextRegionAccessBuilder; +import org.eclipse.xtext.preferences.ITypedPreferenceValues; +import org.eclipse.xtext.resource.XtextResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Provider; + +public class XtextResourceFormatter implements ResourceFormatterService { + private static Logger LOGGER = LoggerFactory.getLogger(XtextResourceFormatter.class); + @Inject + private Provider formatterRequestProvider; + + @Inject + private Provider iFormatter2Provider; + + @Inject + private TextRegionAccessBuilder regionBuilder; + + @Override + public void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues) { + resources.stream().forEach(resource -> { + if (resource instanceof XtextResource) { + formatXtextResource((XtextResource) resource, preferenceValues); + + } else { + LOGGER.debug("Resource is not of type XtextResource and will be skipped: " + resource.getURI()); + } + }); + } + + @Override + public void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues) { + // setup request and formatter + FormatterRequest req = formatterRequestProvider.get(); + req.setPreferences(preferenceValues); + IFormatter2 formatter = iFormatter2Provider.get(); + + ITextRegionAccess regionAccess = regionBuilder.forNodeModel(resource).create(); + req.setTextRegionAccess(regionAccess); + + // list contains all the replacements which should be applied to resource + List replacements = formatter.format(req); + + // formatting using TextRegionRewriter + ITextRegionRewriter regionRewriter = regionAccess.getRewriter(); + String formattedString = regionRewriter.renderToString(regionAccess.regionForDocument(), replacements); + + // With the formatted text, update the resource + InputStream resultStream = new ByteArrayInputStream(formattedString.getBytes(StandardCharsets.UTF_8)); + resource.unload(); + try { + resource.load(resultStream, null); + } catch (IOException e) { + throw new UncheckedIOException( + "Since the resource is an in-memory string, this exception is not expected to be ever thrown.", e); + } + } +} diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java new file mode 100644 index 000000000..9c38fcdea --- /dev/null +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java @@ -0,0 +1,75 @@ +package com.regnosys.rosetta.formatting2; + +import javax.inject.Inject; +import javax.inject.Provider; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.xtext.serializer.ISerializer; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.io.Resources; +import com.regnosys.rosetta.tests.RosettaInjectorProvider; + +@ExtendWith(InjectionExtension.class) +@InjectWith(RosettaInjectorProvider.class) +public class ResourceFormatterServiceTest { + @Inject + ResourceFormatterService formatterService; + @Inject + Provider resourceSetProvider; + @Inject + ISerializer serializer; + + private void testFormatting(Collection inputUrls, Collection expectedUrls) + throws IOException, URISyntaxException { + ResourceSet resourceSet = resourceSetProvider.get(); + List resources = new ArrayList<>(); + List expected = new ArrayList<>(); + + for (String url : inputUrls) { + Resource resource = resourceSet.getResource(URI.createURI(Resources.getResource(url).toString()), true); + resources.add(resource); + } + + for (String url : expectedUrls) { + expected.add(Files.readString(Path.of(Resources.getResource(url).toURI()))); + } + + formatterService.formatCollection(resources); + + List result = resources.stream().map(resource -> serializer.serialize(resource.getContents().get(0))) + .collect(Collectors.toList()); + + Assertions.assertIterableEquals(expected, result); + } + + @Test + void formatSingleDocument() throws IOException, URISyntaxException { + testFormatting(List.of("formatting-test/input/typeAlias.rosetta"), + List.of("formatting-test/expected/typeAlias.rosetta")); + } + + @Test + void formatMultipleDocuments() throws IOException, URISyntaxException { + testFormatting( + List.of("formatting-test/input/typeAlias.rosetta", + "formatting-test/input/typeAliasWithDocumentation.rosetta"), + List.of("formatting-test/expected/typeAlias.rosetta", + "formatting-test/expected/typeAliasWithDocumentation.rosetta")); + } +} diff --git a/rosetta-testing/src/test/resources/formatting-test/expected/typeAlias.rosetta b/rosetta-testing/src/test/resources/formatting-test/expected/typeAlias.rosetta new file mode 100644 index 000000000..a1e1a60e7 --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/expected/typeAlias.rosetta @@ -0,0 +1,3 @@ +namespace test + +typeAlias maxNBoundedNumber(n int, max number): number(digits: n, max: max) diff --git a/rosetta-testing/src/test/resources/formatting-test/expected/typeAliasWithDocumentation.rosetta b/rosetta-testing/src/test/resources/formatting-test/expected/typeAliasWithDocumentation.rosetta new file mode 100644 index 000000000..e00163185 --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/expected/typeAliasWithDocumentation.rosetta @@ -0,0 +1,7 @@ +namespace test + +typeAlias maxNBoundedNumber( + n int + , max number <"The maximum bound on this number. If absent, this number is unbounded from above."> +): <"A bounded decimal number with N maximum number of digits."> + number(digits: n, max: max) diff --git a/rosetta-testing/src/test/resources/formatting-test/input/typeAlias.rosetta b/rosetta-testing/src/test/resources/formatting-test/input/typeAlias.rosetta new file mode 100644 index 000000000..ee02cd6b9 --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/input/typeAlias.rosetta @@ -0,0 +1,7 @@ +namespace test + + + typeAlias maxNBoundedNumber + ( n int ,max number) + : + number ( digits : n , max: max ) \ No newline at end of file diff --git a/rosetta-testing/src/test/resources/formatting-test/input/typeAliasWithDocumentation.rosetta b/rosetta-testing/src/test/resources/formatting-test/input/typeAliasWithDocumentation.rosetta new file mode 100644 index 000000000..01be6d9fe --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/input/typeAliasWithDocumentation.rosetta @@ -0,0 +1,8 @@ +namespace test + + + typeAlias maxNBoundedNumber + ( n int + ,max number <"The maximum bound on this number. If absent, this number is unbounded from above.">) + : <"A bounded decimal number with N maximum number of digits."> + number ( digits : n , max: max ) diff --git a/rosetta-tools/pom.xml b/rosetta-tools/pom.xml index e64d5504c..c98e09510 100644 --- a/rosetta-tools/pom.xml +++ b/rosetta-tools/pom.xml @@ -94,6 +94,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jdk8 + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + diff --git a/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java b/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java new file mode 100644 index 000000000..3981db8f8 --- /dev/null +++ b/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java @@ -0,0 +1,80 @@ +package com.regnosys.rosetta.tools; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Injector; +import com.regnosys.rosetta.RosettaStandaloneSetup; +import com.regnosys.rosetta.formatting2.ResourceFormatterService; +import com.regnosys.rosetta.formatting2.XtextResourceFormatter; + +/** + * A command-line tool for formatting `.rosetta` files in a specified directory. + *

+ * This tool uses the {@link ResourceFormatterService} to apply consistent formatting to each + * `.rosetta` file in the provided directory. It loads each file as a resource, applies + * formatting in-place, and saves the modified file back to disk. The tool can be run with a + * single directory path argument, which is used to locate `.rosetta` files. + *

+ * + *

Usage:

+ *
+ * java ResourceFormattingTool /path/to/directory
+ * 
+ *

+ * If no valid directory path is provided as an argument, the program will exit with an error message. + *

+ */ +public class ResourceFormattingTool { + private static Logger LOGGER = LoggerFactory.getLogger(ResourceFormattingTool.class); + + public static void main(String[] args) { + if (args.length == 0) { + System.out.println("Please provide the directory path as an argument."); + System.exit(1); + } + + Path directory = Paths.get(args[0]); + if (!Files.isDirectory(directory)) { + System.out.println("The provided path is not a valid directory."); + System.exit(1); + } + + Injector inj = new RosettaStandaloneSetup().createInjectorAndDoEMFRegistration(); + ResourceSet resourceSet = inj.getInstance(ResourceSet.class); + ResourceFormatterService formatterService = inj.getInstance(ResourceFormatterService.class); + + try { + // Find all .rosetta files in the directory and load them from disk + List resources = Files.walk(directory) + .filter(path -> path.toString().endsWith(".rosetta")) + .map(file -> resourceSet.getResource(URI.createFileURI(file.toString()), true)) + .collect(Collectors.toList()); + + // format resources + formatterService.formatCollection(resources, null); + + // save each resource + resources.forEach(resource -> { + try { + resource.save(null); + LOGGER.info("Successfully formatted and saved file at location " + resource.getURI()); + } catch (IOException e) { + LOGGER.error("Error saving file at location " + resource.getURI() + ": "+ e.getMessage()); + } + }); + } catch (IOException e) { + LOGGER.error("Error processing files: " + e.getMessage()); + } + } +}