Skip to content

Commit 0c66ca7

Browse files
author
Killian Perlin
committed
Reorganize refactorings
Cleaning up Refactoring interface, splitting it into 2 interfaces. Removed lkt_refactor flag from some tests since the commit revealed a bug that made them pass while they shouldn't.
1 parent 6794a62 commit 0c66ca7

File tree

10 files changed

+238
-186
lines changed

10 files changed

+238
-186
lines changed

lkql_jit/cli/src/main/java/com/adacore/lkql_jit/cli/LKQLRefactor.java

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,40 @@
99

1010
import com.adacore.lkql_jit.options.Refactorings.LKQLToLkt;
1111
import com.adacore.lkql_jit.options.Refactorings.Refactoring;
12+
import com.adacore.lkql_jit.options.Refactorings.TokenBasedRefactoring;
13+
import java.io.PrintWriter;
14+
import java.nio.charset.StandardCharsets;
1215
import java.util.ArrayList;
1316
import java.util.List;
1417
import java.util.concurrent.Callable;
1518
import picocli.CommandLine;
1619

1720
/**
18-
* Refactor command for LKQL. Allows to run automatic migrations, allowing the LKQL team to create
19-
* migrators to adapt to syntactic or semantic changes in LKQL.
21+
* Refactor command for LKQL. Allows to run automatic migrations, allowing the
22+
* LKQL team to create migrators to adapt to syntactic or semantic changes in
23+
* LKQL.
2024
*
21-
* <p>Migrators work on the stream of tokens directly. Migrator implementer will attach actions
22-
* (append/prepend/replace) on tokens, which allows to modify the output stream without working on
23-
* text directly, which simplifies the expression of refactorings.
25+
* <p>
26+
* Migrators are just an implementation of the functionnal interface
27+
* 'Refactoring'. It expects an LKQL unit as input and produces the new unit
28+
* content as a 'String'.
2429
*
25-
* <p>A migrator implementer will typically:
30+
* <p>
31+
* In practice, a 'Refactoring' implementation will be of one of 2 kinds:
32+
* 1. A 'TokenBasedRefactoring' working on the stream of tokens directly and
33+
* producing rewriting actions on each token of the stream.
34+
* 2. A 'TreeBasedRefactoring' working on the Liblkqllang syntax tree and
35+
* producing a new 'String' directly.
2636
*
27-
* <p>1. Find the nodes he wants to refactor in the LKQL tree 2. From those nodes, find the tokens
28-
* inside the nodes that need to be altered 3. Attach actions to those tokens
37+
* <p>
38+
* Then the rewriter will emit a new file for every LKQL unit, either inplace or
39+
* on stdout.
2940
*
30-
* <p>Then the rewriter will emit a new file for every LKQL unit, either inplace or on stdout.
41+
* <p>
42+
* To add a refactoring, you need to extend the 'RefactoringKind' enum to add a
43+
* new refactoring id, and then extend the 'getRefactoring' method to return the
44+
* corresponding implementation of 'Refactoring' to apply.
3145
*
32-
* <p>To add a refactoring, you need to extend the 'refactoringKind' enum to add a new refactoring
33-
* id, and then extend the 'getRefactoring' method to return an anonymous function that will add
34-
* actions to the list of actions.
3546
*/
3647
@CommandLine.Command(
3748
name = "refactor",
@@ -66,7 +77,7 @@ private enum RefactoringKind {
6677
*/
6778
public Refactoring getRefactoring(AnalysisUnit unit) {
6879
return switch (refactoring) {
69-
case IS_TO_COLON -> (Refactoring.State state) -> {
80+
case IS_TO_COLON -> (TokenBasedRefactoring) state -> {
7081
Refactoring.stream(state.unit.getRoot())
7182
.filter(n -> n instanceof NodePatternDetail)
7283
.forEach(det -> {
@@ -89,15 +100,26 @@ public Refactoring getRefactoring(AnalysisUnit unit) {
89100
};
90101
}
91102

103+
/**
104+
* Rewrite a unit by printing refactored file, either to stdout, or to the
105+
* original file, depending on the value of the '-i' command line flag.
106+
*/
92107
@Override
93108
public Integer call() {
94109
var ctx = AnalysisContext.create();
95110
for (var file : files) {
96111
var unit = ctx.getUnitFromFile(file);
97-
var refactoring = getRefactoring(unit);
98-
var state = new Refactoring.State(unit);
99-
refactoring.applyRefactor(state);
100-
state.printAllTokens(this.inPlace);
112+
var result = getRefactoring(unit).apply(unit);
113+
114+
if (inPlace) {
115+
try (var writer = new PrintWriter(unit.getFileName(), StandardCharsets.UTF_8)) {
116+
writer.print(result);
117+
} catch (Exception e) {
118+
throw new RuntimeException(e);
119+
}
120+
} else {
121+
System.out.print(result);
122+
}
101123
}
102124
return 0;
103125
}

lkql_jit/options/src/main/java/com/adacore/lkql_jit/options/Refactorings/LKQLToLkt.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@
99

1010
import com.adacore.liblkqllang.Liblkqllang;
1111

12-
public class LKQLToLkt implements Refactoring {
12+
public class LKQLToLkt implements TreeBasedRefactoring {
1313

1414
/** Pointer to last-entered selector during rewriting. */
1515
private Liblkqllang.SelectorDecl currentSelector = Liblkqllang.SelectorDecl.NONE;
1616

1717
@Override
18-
public void applyRefactor(Refactoring.State state) {
19-
final var root = state.unit.getRoot();
20-
state.prepend(root.tokenStart(), "# lkql version: 2\n\n");
18+
public String apply(Liblkqllang.AnalysisUnit unit) {
19+
var root = unit.getRoot();
20+
return (
21+
"# lkql version: 2\n\n" +
22+
textRange(unit.getFirstToken(), root.tokenStart().previous()) +
23+
apply(root) +
24+
textRange(root.tokenEnd().next(), unit.getLastToken())
25+
);
26+
}
2127

22-
// Because this rewriting doesn't use the token/action API, this is a hack
23-
state.replaceRange(root.tokenStart(), root.tokenEnd(), refactorNode(root));
28+
public String apply(Liblkqllang.LkqlNode root) {
29+
return refactorNode(root);
2430
}
2531

2632
/** Computes the text representing the refactored node. */

lkql_jit/options/src/main/java/com/adacore/lkql_jit/options/Refactorings/Refactoring.java

Lines changed: 4 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -6,167 +6,14 @@
66
package com.adacore.lkql_jit.options.Refactorings;
77

88
import com.adacore.liblkqllang.Liblkqllang;
9-
import java.io.PrintWriter;
10-
import java.io.StringWriter;
11-
import java.nio.charset.StandardCharsets;
12-
import java.util.ArrayList;
13-
import java.util.HashMap;
14-
import java.util.List;
159
import java.util.function.Consumer;
1610
import java.util.stream.Stream;
1711

12+
/** Class offering an abstraction over all refactoring processes. */
13+
@FunctionalInterface
1814
public interface Refactoring {
19-
class State {
20-
21-
public final Liblkqllang.AnalysisUnit unit;
22-
23-
/** Kind of actions that can be attached to a token. */
24-
enum ActionKind {
25-
APPEND,
26-
PREPEND,
27-
REPLACE,
28-
}
29-
30-
/**
31-
* Action record, encapsulating an action that can be attached to a token. When applied, the
32-
* action will either append the given text, prepend the given text, or replace the token text
33-
* with the given text, depending on the kind.
34-
*
35-
* <p>To remove a token, simply use a REPLACE action with an empty text.
36-
*/
37-
private record Action(ActionKind kind, String text) {}
38-
39-
/**
40-
* List of actions accumulated in this state object.
41-
*/
42-
public final HashMap<String, List<Action>> actions = new HashMap<>();
43-
44-
public State(Liblkqllang.AnalysisUnit unit) {
45-
this.unit = unit;
46-
}
47-
48-
/** Add an action to the list of actions to apply */
49-
private void addAction(Liblkqllang.Token token, Action action) {
50-
List<Action> actionList;
51-
var tokenId = getTokenId(token);
52-
if (actions.containsKey(tokenId)) {
53-
actionList = actions.get(tokenId);
54-
} else {
55-
actionList = new ArrayList<>();
56-
actions.put(tokenId, actionList);
57-
}
58-
59-
actionList.add(action);
60-
}
61-
62-
public void delete(Liblkqllang.Token token) {
63-
addAction(token, new Action(ActionKind.REPLACE, ""));
64-
}
65-
66-
public void replace(Liblkqllang.Token token, String text) {
67-
addAction(token, new Action(ActionKind.REPLACE, text));
68-
}
69-
70-
public void replaceRange(Liblkqllang.Token start, Liblkqllang.Token end, String text) {
71-
var cur = start;
72-
while (cur.tokenIndex != end.tokenIndex) {
73-
delete(cur);
74-
cur = cur.next();
75-
}
76-
// append new code
77-
replace(end, text);
78-
}
79-
80-
public void append(Liblkqllang.Token token, String text) {
81-
addAction(token, new Action(ActionKind.APPEND, text));
82-
}
83-
84-
public void prepend(Liblkqllang.Token token, String text) {
85-
addAction(token, new Action(ActionKind.PREPEND, text));
86-
}
87-
88-
/**
89-
* Internal method: rewrite a unit by printing all the tokens via the 'write' callback. Apply
90-
* the actions at the same time.
91-
*/
92-
private void printAllTokens(Consumer<String> write) {
93-
for (var tok = unit.getFirstToken(); !tok.isNone(); tok = tok.next()) {
94-
var tokActions = actions.get(getTokenId(tok));
95-
if (tokActions != null) {
96-
var replaceActions = tokActions
97-
.stream()
98-
.filter(c -> c.kind == ActionKind.REPLACE)
99-
.toList();
100-
assert replaceActions.size() <= 1 : "Only one replace action per token";
101-
102-
var prependActions = tokActions
103-
.stream()
104-
.filter(c -> c.kind == ActionKind.PREPEND)
105-
.toList();
106-
107-
var appendActions = tokActions
108-
.stream()
109-
.filter(c -> c.kind == ActionKind.APPEND)
110-
.toList();
111-
112-
for (var action : prependActions) {
113-
write.accept(action.text);
114-
}
115-
116-
if (!replaceActions.isEmpty()) {
117-
write.accept(replaceActions.get(0).text);
118-
} else {
119-
write.accept(tok.getText());
120-
}
121-
122-
for (var action : appendActions) {
123-
write.accept(action.text);
124-
}
125-
} else {
126-
write.accept(tok.getText());
127-
}
128-
}
129-
}
130-
131-
/**
132-
* Rewrite a unit by printing all the tokens, either to stdout, or to the
133-
* original file, depending on the value of the '-i' command line flag.
134-
*/
135-
public void printAllTokens(boolean inPlace) {
136-
try {
137-
if (inPlace) {
138-
var writer = new PrintWriter(unit.getFileName(), StandardCharsets.UTF_8);
139-
printAllTokens(writer::print);
140-
writer.close();
141-
} else {
142-
printAllTokens(System.out::print);
143-
}
144-
} catch (Exception e) {
145-
throw new RuntimeException(e);
146-
}
147-
}
148-
149-
public String toString() {
150-
var writer = new StringWriter();
151-
printAllTokens(writer::write);
152-
return writer.toString();
153-
}
154-
}
155-
156-
/**
157-
* Helper to create a unique Id for a token. TODO: This method exists only because the
158-
* 'hashCode' method on Tokens is wrong. This should be fixed at the Langkit level. See
159-
* eng/libadalang/langkit#857.
160-
*
161-
* @return the id as a string
162-
*/
163-
private static String getTokenId(Liblkqllang.Token token) {
164-
return (
165-
token.unit.getFileName() +
166-
(token.isTrivia() ? "trivia " + token.triviaIndex : token.tokenIndex) +
167-
token.kind
168-
);
169-
}
15+
/** Take a parsing tree root and return its source after refactoring actions. */
16+
String apply(Liblkqllang.AnalysisUnit unit);
17017

17118
public static boolean isWhitespace(Liblkqllang.Token token) {
17219
return token.kind == Liblkqllang.TokenKind.LKQL_WHITESPACE;
@@ -198,6 +45,4 @@ public static Stream<Liblkqllang.LkqlNode> stream(Liblkqllang.LkqlNode root) {
19845
public static Stream<Liblkqllang.Token> streamFrom(Liblkqllang.Token start) {
19946
return Stream.iterate(start, t -> !t.isNone(), t -> t.next());
20047
}
201-
202-
void applyRefactor(State state);
20348
}

0 commit comments

Comments
 (0)