eclipse-epsilon / epsilon

Epsilon is a family of Java-based scripting languages for automating common model-based software engineering tasks, such as code generation, model-to-model transformation and model validation, that work out of the box with EMF (including Xtext and Sirius), UML (including Cameo/MagicDraw), Simulink, XML and other types of models.
https://eclipse.org/epsilon
Eclipse Public License 2.0
67 stars 10 forks source link

Outdentation breaks trace links #128

Open kolovos opened 1 month ago

kolovos commented 1 month ago

EGL's recently-introduced outdentation feature, breaks traceability links. For example, if the template from the EGX playground example is modified as follows (outdentation added in line 6)

[*Generate a <h1> with the name of the person*]
<h1>[%=p.name%]'s Tasks</h1> 
[*Generate a table for the person's tasks*]
<table>
[*For every task*]
[%for (t in p.getTasks()){-%]
[*Generate a row with the title of the task*]
    <tr>
        <td>[%=t.title%]</td>
    </tr>
[%}%]
</table>

[%
// Returns the tasks of a person
operation Person getTasks() {
    return Task.all.select(
        t|t.effort.exists(e|e.person = self));
}
%]

the reported trace links for task titles are broken (see below)

image

This is because outdentation is implemented partly using a post-transformation formatter, and formatters are expected to deal with updating traceability themselves (which the outdentation formatter doesn't).

Given that most formatters only add/remove whitespace, we could introduce an abstract e.g. TraceabilityPreservingFormatter class that updates trace links given only the original and the formatted text and make EGL's OutdentationFormatter, as well as other formatters, extend it. To deal with cases where formatters actually do more than adding/removing whitespace, TraceabilityPreservingFormatter could actually check that the original/formatted text only differ in whitespace and fail or report a warning otherwise.

kolovos commented 1 week ago

Below is some code we could use to map offsets of formatted code back to the original code assuming the formatter has only added/removed whitespace (which is the case for the outdentation formatter)

package org.eclipse.epsilon.egl.formatter;

import java.util.ArrayList;
import java.util.List;

public class PositionMapper {

    public static List<Integer> mapOffsets(String original, String formatted) {

        // Helper variables to track the current index in both strings
        int originalIndex = 0, formattedIndex = 0;

        // Create an array to store the mapped offsets of each character in the original string
        int[] mappedOffsets = new int[original.length()];

        // Process both strings simultaneously
        while (originalIndex < original.length() && formattedIndex < formatted.length()) {

            char originalChar = original.charAt(originalIndex);
            char formattedChar = formatted.charAt(formattedIndex);

            // If characters match, map the current formatted index to the original index
            if (formattedIndex < formatted.length() && originalChar == formattedChar) {
                mappedOffsets[originalIndex] = formattedIndex;
                originalIndex++;
                formattedIndex++;
            } else if (Character.isWhitespace(originalChar)) {
                // Handle cases where the characters differ by continuing through the original string
                mappedOffsets[originalIndex] = -1;
                originalIndex++;
            } else if (Character.isWhitespace(formattedChar)) {
                formattedIndex++;
            }
        }

        // If there are remaining characters in the original string that weren't mapped yet
        while (originalIndex < original.length()) {
            mappedOffsets[originalIndex] = formattedIndex;  // Remaining original chars map to end of formatted
            originalIndex++;
        }

        ArrayList<Integer> mappedOffsetsList = new ArrayList<Integer>();
        for (int mappedOffset : mappedOffsets) mappedOffsetsList.add(mappedOffset);
        return mappedOffsetsList;
    }

    public static void main(String[] args) {
        // Example usage
        String original = "Hello\t\n\nWorld!";
        String formatted = "Hello\n\t\t World!";
        List<Integer> originalOffsets = new ArrayList<>();
        originalOffsets.add(0);  // H
        originalOffsets.add(6);  // W

        List<Integer> formattedOffsets = mapOffsets(original, formatted);

        int originalOffset = 0;
        for (Integer mappedOffset : formattedOffsets) {
            char mappedChar = mappedOffset >= 0 ? formatted.charAt(mappedOffset) : ' ';
            System.out.println(originalOffset + "->" + mappedOffset + " / " + original.charAt(originalOffset) + "->" + mappedChar);
            originalOffset++;
        }
    }
}