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

Automating Formatting of Rune Code #859

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

maria77102
Copy link

This PR deals with issue #858.

Type of change

  • New feature (non-breaking change which adds functionality)

Copy link

linux-foundation-easycla bot commented Oct 28, 2024

CLA Not Signed

import org.eclipse.emf.ecore.resource.Resource;

public interface CodeFormatterService {
List<Resource> formatCollection(List<Resource> resources) throws IOException;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this method modifies the resources in-place, I would change this to void. A bit of documentation to explain this behaviour would also be good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the type of the input can be loosened to Collection.


import org.eclipse.emf.ecore.resource.Resource;

public interface CodeFormatterService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is perhaps a bit generic - it doesn't really tell you what it does. What about ResourceFormatterService?

Resource formattedDocument = formatDocument(doc);
result.add(formattedDocument);
}
return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is perfectly fine, but just as a challenge, could you rewrite this using Java stream API? :)

private Provider<FormatterRequest> formatterRequestProvider;

@Inject
private Provider<RosettaFormatter> rosettaFormatterProvider;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is injecting the implementation rather than the interface. Change to inject the formatter interface instead.

Just to explain why: this is against the principle of dependency injection, since it breaks the separation between interface and implementation, making the codebase less extensible.

return result;
}

private Resource formatDocument(Resource doc) throws IOException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the word Document is used for something specific in Xtext's formatting code (e.g., IFormattableDocument), the name formatDocument might be misleading. What about formatResource?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should expose this method in the interface as well. It might be good if at some point a use case arises for formatting a single resource.

import javax.inject.Inject;
import javax.inject.Provider;

public class RuneFormatter implements CodeFormatterService{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make name of class consistent with interface.

INode node = NodeModelUtils.getNode(content);

TextRegion textRegion = new TextRegion(node.getOffset(), node.getLength());
req.setRegions(Collections.singletonList(textRegion));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at the documentation of regions inside FormatterRequest. I think this can be simplified.

TextRegion textRegion = new TextRegion(node.getOffset(), node.getLength());
req.setRegions(Collections.singletonList(textRegion));

ITextRegionAccess regionAccess = regionBuilder.forNodeModel((XtextResource) doc).create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. I don't think we should cast here.

Just some background: in our case, all resources are XtextResources for now. However, there is support for adding other kinds of resources as well to a resource set. Suppose for example that we somehow would add support for loading .csv files or .xml files or other "raw" resources, then this will crash.

Instead what I think we should do is change the input type to XtextResource (because other resources are not supported), and then in the method formatCollection filter out any non-XtextResources.



//formatting using TextRegionRewriter
TextRegionRewriter regionRewriter = new TextRegionRewriter(regionAccess);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See docs on class for the recommended way of getting an instance. :)


private Resource formatDocument(Resource doc) throws IOException {
//setup request and formatter
FormatterRequest req = formatterRequestProvider.get();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should add some configuration to this request as well. (perhaps even a way to pass in ITypedPreferenceValues to configure the formatting?)


@ExtendWith(InjectionExtension.class)
@InjectWith(RosettaInjectorProvider.class)
public class FormatterServiceTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have a consistent name, e.g., <ServiceInterfaceName>Test

@Inject ISerializer serializer;

@Test
void formatSingleDocument() throws IOException, URISyntaxException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since formatSingleDocument and formatMultipleDocuments share a lot of code, I think it would improve maintainability if we added a little test utility method to this class. E.g., you pass it a list of files, and the test will format those files and compare them to their expectation.

@Override
public void formatCollection(Collection<Resource> resources) {
formatCollection(resources, null);
}
Copy link
Contributor

@SimonCockx SimonCockx Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer trivial implementations such as these as a default interface method instead, so implementers of the interface do not have to write the same boilerplate code. I would move this one up, together with the two below.

e.printStackTrace();
}
} else {
System.out.println("Resource is not of type XtextResource and will be skipped: " + resource);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me introduce you to the wonderful world of logging APIs :)

}

@Override
public void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues) throws IOException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised to see a throws IOException in this declaration, since conceptually this method does not perform any I/O. More below.

//With the formatted text, update the resource
InputStream resultStream = new ByteArrayInputStream(formattedString.getBytes(StandardCharsets.UTF_8));
resource.unload();
resource.load(resultStream, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you added the IOException to the declaration because this method potentially throws an IOException. This is because in general the input stream could come from anywhere, e.g., from reading a file, or from a HTTP request over the internet. However, we know that the input stream is actually coming from an in-memory string (so no I/O happening), so in practice I would not expect an IOException here, so I don't think we should leak it to the callers of this method.

That's a long way of saying I think we should catch the error here, wrap it in an UncheckedIOException, and then rethrow it. :) (together with a small comment explaining why we expect an IOException never to happen)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants