Skip to content

Commit

Permalink
Automating Formatting of Rune Code (#859)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maria77102 authored Nov 7, 2024
1 parent fb297a7 commit 8827ec7
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -148,4 +150,8 @@ class RosettaRuntimeModule extends AbstractRosettaRuntimeModule {
def Class<? extends IRequestScopedCache> bindIRequestScopedCache() {
RequestScopedCache
}

def Class<? extends ResourceFormatterService> bindResourceFormatterService() {
XtextResourceFormatter
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*
* @param resources a collection of {@link XtextResource} objects to be formatted
*/
default void formatCollection(Collection<Resource> resources) {
formatCollection(resources, null);
}

/**
* Formats the given {@link XtextResource} in-memory.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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}.
* </p>
*
* @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<Resource> resources, ITypedPreferenceValues preferenceValues);

/**
* Formats the given {@link XtextResource} in-memory.
* <p>
* 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}.
* </p>
*
* @param resource the {@link XtextResource} to format
*/
void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues);
}
Original file line number Diff line number Diff line change
@@ -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<FormatterRequest> formatterRequestProvider;

@Inject
private Provider<IFormatter2> iFormatter2Provider;

@Inject
private TextRegionAccessBuilder regionBuilder;

@Override
public void formatCollection(Collection<Resource> 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<ITextReplacement> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ResourceSet> resourceSetProvider;
@Inject
ISerializer serializer;

private void testFormatting(Collection<String> inputUrls, Collection<String> expectedUrls)
throws IOException, URISyntaxException {
ResourceSet resourceSet = resourceSetProvider.get();
List<Resource> resources = new ArrayList<>();
List<String> 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<String> 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"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace test

typeAlias maxNBoundedNumber(n int, max number): number(digits: n, max: max)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace test


typeAlias maxNBoundedNumber
( n int ,max number)
:
number ( digits : n , max: max )
Original file line number Diff line number Diff line change
@@ -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 )
8 changes: 8 additions & 0 deletions rosetta-tools/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* </p>
*
* <h2>Usage:</h2>
* <pre>
* java ResourceFormattingTool /path/to/directory
* </pre>
* <p>
* If no valid directory path is provided as an argument, the program will exit with an error message.
* </p>
*/
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<Resource> 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());
}
}
}

0 comments on commit 8827ec7

Please sign in to comment.