1 | /* |
---|---|
2 | * Copyright (C) 2007-2010 JĂșlio Vilmar Gesser. |
3 | * Copyright (C) 2011, 2013-2020 The JavaParser Team. |
4 | * |
5 | * This file is part of JavaParser. |
6 | * |
7 | * JavaParser can be used either under the terms of |
8 | * a) the GNU Lesser General Public License as published by |
9 | * the Free Software Foundation, either version 3 of the License, or |
10 | * (at your option) any later version. |
11 | * b) the terms of the Apache License |
12 | * |
13 | * You should have received a copy of both licenses in LICENCE.LGPL and |
14 | * LICENCE.APACHE. Please refer to those files for details. |
15 | * |
16 | * JavaParser is distributed in the hope that it will be useful, |
17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | * GNU Lesser General Public License for more details. |
20 | */ |
21 | |
22 | package com.github.javaparser.printer.lexicalpreservation; |
23 | |
24 | import static com.github.javaparser.GeneratedJavaParserConstants.LBRACE; |
25 | import static com.github.javaparser.GeneratedJavaParserConstants.RBRACE; |
26 | import static com.github.javaparser.GeneratedJavaParserConstants.SPACE; |
27 | |
28 | import java.util.ArrayList; |
29 | import java.util.Comparator; |
30 | import java.util.EnumMap; |
31 | import java.util.HashMap; |
32 | import java.util.LinkedList; |
33 | import java.util.List; |
34 | import java.util.ListIterator; |
35 | import java.util.Map; |
36 | import java.util.Optional; |
37 | |
38 | import com.github.javaparser.GeneratedJavaParserConstants; |
39 | import com.github.javaparser.JavaToken; |
40 | import com.github.javaparser.JavaToken.Kind; |
41 | import com.github.javaparser.TokenTypes; |
42 | import com.github.javaparser.ast.Node; |
43 | import com.github.javaparser.ast.NodeList; |
44 | import com.github.javaparser.ast.comments.Comment; |
45 | import com.github.javaparser.ast.nodeTypes.NodeWithTypeArguments; |
46 | import com.github.javaparser.ast.type.Type; |
47 | import com.github.javaparser.printer.concretesyntaxmodel.CsmElement; |
48 | import com.github.javaparser.printer.concretesyntaxmodel.CsmIndent; |
49 | import com.github.javaparser.printer.concretesyntaxmodel.CsmMix; |
50 | import com.github.javaparser.printer.concretesyntaxmodel.CsmToken; |
51 | import com.github.javaparser.printer.concretesyntaxmodel.CsmUnindent; |
52 | import com.github.javaparser.printer.lexicalpreservation.LexicalDifferenceCalculator.CsmChild; |
53 | |
54 | /** |
55 | * A Difference should give me a sequence of elements I should find (to indicate the context) followed by a list of elements |
56 | * to remove or to add and follow by another sequence of elements. |
57 | * |
58 | * I should later be able to apply such difference to a nodeText. |
59 | */ |
60 | public class Difference { |
61 | |
62 | public static final int STANDARD_INDENTATION_SIZE = 4; |
63 | |
64 | private final NodeText nodeText; |
65 | private final Node node; |
66 | |
67 | private final List<DifferenceElement> diffElements; |
68 | private final List<TextElement> originalElements; |
69 | private int originalIndex = 0; |
70 | private int diffIndex = 0; |
71 | |
72 | private final List<TokenTextElement> indentation; |
73 | private boolean addedIndentation = false; |
74 | |
75 | Difference(List<DifferenceElement> diffElements, NodeText nodeText, Node node) { |
76 | if (nodeText == null) { |
77 | throw new NullPointerException("nodeText can not be null"); |
78 | } |
79 | |
80 | this.nodeText = nodeText; |
81 | this.node = node; |
82 | this.diffElements = diffElements; |
83 | this.originalElements = nodeText.getElements(); |
84 | |
85 | this.indentation = LexicalPreservingPrinter.findIndentation(node); |
86 | } |
87 | |
88 | private List<TextElement> processIndentation(List<TokenTextElement> indentation, List<TextElement> prevElements) { |
89 | List<TextElement> res = new LinkedList<>(indentation); |
90 | boolean afterNl = false; |
91 | for (TextElement e : prevElements) { |
92 | if (e.isNewline()) { |
93 | res.clear(); |
94 | afterNl = true; |
95 | } else { |
96 | if (afterNl && e instanceof TokenTextElement && TokenTypes.isWhitespace(((TokenTextElement)e).getTokenKind())) { |
97 | res.add(e); |
98 | } else { |
99 | afterNl = false; |
100 | } |
101 | } |
102 | } |
103 | return res; |
104 | } |
105 | |
106 | private List<TextElement> indentationBlock() { |
107 | List<TextElement> res = new LinkedList<>(); |
108 | res.add(new TokenTextElement(SPACE)); |
109 | res.add(new TokenTextElement(SPACE)); |
110 | res.add(new TokenTextElement(SPACE)); |
111 | res.add(new TokenTextElement(SPACE)); |
112 | return res; |
113 | } |
114 | |
115 | private boolean isAfterLBrace(NodeText nodeText, int nodeTextIndex) { |
116 | if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isToken(LBRACE)) { |
117 | return true; |
118 | } |
119 | if (nodeTextIndex > 0 && nodeText.getElements().get(nodeTextIndex - 1).isSpaceOrTab()) { |
120 | return isAfterLBrace(nodeText, nodeTextIndex - 1); |
121 | } |
122 | return false; |
123 | } |
124 | |
125 | /** |
126 | * If we are at the beginning of a line, with just spaces or tabs before us we should force the space to be |
127 | * the same as the indentation. |
128 | */ |
129 | private int considerEnforcingIndentation(NodeText nodeText, int nodeTextIndex) { |
130 | boolean hasOnlyWsBefore = true; |
131 | for (int i = nodeTextIndex; i >= 0 && hasOnlyWsBefore && i < nodeText.getElements().size(); i--) { |
132 | if (nodeText.getElements().get(i).isNewline()) { |
133 | break; |
134 | } |
135 | if (!nodeText.getElements().get(i).isSpaceOrTab()) { |
136 | hasOnlyWsBefore = false; |
137 | } |
138 | } |
139 | int res = nodeTextIndex; |
140 | if (hasOnlyWsBefore) { |
141 | for (int i = nodeTextIndex; i >= 0 && i < nodeText.getElements().size(); i--) { |
142 | if (nodeText.getElements().get(i).isNewline()) { |
143 | break; |
144 | } |
145 | nodeText.removeElement(i); |
146 | res = i; |
147 | } |
148 | } |
149 | if (res < 0) { |
150 | throw new IllegalStateException(); |
151 | } |
152 | return res; |
153 | } |
154 | |
155 | /** |
156 | * Node that we have calculate the Difference we can apply to a concrete NodeText, modifying it according |
157 | * to the difference (adding and removing the elements provided). |
158 | */ |
159 | void apply() { |
160 | extractReshuffledDiffElements(diffElements); |
161 | Map<Removed, RemovedGroup> removedGroups = combineRemovedElementsToRemovedGroups(); |
162 | |
163 | do { |
164 | boolean isLeftOverDiffElement = applyLeftOverDiffElements(); |
165 | boolean isLeftOverOriginalElement = applyLeftOverOriginalElements(); |
166 | |
167 | if (!isLeftOverDiffElement && !isLeftOverOriginalElement){ |
168 | DifferenceElement diffElement = diffElements.get(diffIndex); |
169 | |
170 | if (diffElement instanceof Added) { |
171 | applyAddedDiffElement((Added) diffElement); |
172 | } else { |
173 | TextElement originalElement = originalElements.get(originalIndex); |
174 | boolean originalElementIsChild = originalElement instanceof ChildTextElement; |
175 | boolean originalElementIsToken = originalElement instanceof TokenTextElement; |
176 | |
177 | if (diffElement instanceof Kept) { |
178 | applyKeptDiffElement((Kept) diffElement, originalElement, originalElementIsChild, originalElementIsToken); |
179 | } else if (diffElement instanceof Removed) { |
180 | Removed removed = (Removed) diffElement; |
181 | applyRemovedDiffElement(removedGroups.get(removed), removed, originalElement, originalElementIsChild, originalElementIsToken); |
182 | } else { |
183 | throw new UnsupportedOperationException("" + diffElement + " vs " + originalElement); |
184 | } |
185 | } |
186 | } |
187 | } while (diffIndex < diffElements.size() || originalIndex < originalElements.size()); |
188 | } |
189 | |
190 | private boolean applyLeftOverOriginalElements() { |
191 | boolean isLeftOverElement = false; |
192 | if (diffIndex >= diffElements.size() && originalIndex < originalElements.size()) { |
193 | TextElement originalElement = originalElements.get(originalIndex); |
194 | |
195 | if (originalElement.isWhiteSpaceOrComment()) { |
196 | originalIndex++; |
197 | } else { |
198 | throw new UnsupportedOperationException("NodeText: " + nodeText + ". Difference: " |
199 | + this + " " + originalElement); |
200 | } |
201 | |
202 | isLeftOverElement = true; |
203 | } |
204 | return isLeftOverElement; |
205 | } |
206 | |
207 | private boolean applyLeftOverDiffElements() { |
208 | boolean isLeftOverElement = false; |
209 | if (diffIndex < diffElements.size() && originalIndex >= originalElements.size()) { |
210 | DifferenceElement diffElement = diffElements.get(diffIndex); |
211 | if (diffElement instanceof Kept) { |
212 | Kept kept = (Kept) diffElement; |
213 | |
214 | if (kept.isWhiteSpaceOrComment() || kept.isIndent() || kept.isUnindent()) { |
215 | diffIndex++; |
216 | } else { |
217 | throw new IllegalStateException("Cannot keep element because we reached the end of nodetext: " |
218 | + nodeText + ". Difference: " + this); |
219 | } |
220 | } else if (diffElement instanceof Added) { |
221 | Added addedElement = (Added) diffElement; |
222 | |
223 | nodeText.addElement(originalIndex, addedElement.toTextElement()); |
224 | originalIndex++; |
225 | diffIndex++; |
226 | } else { |
227 | throw new UnsupportedOperationException(diffElement.getClass().getSimpleName()); |
228 | } |
229 | |
230 | isLeftOverElement = true; |
231 | } |
232 | |
233 | return isLeftOverElement; |
234 | } |
235 | |
236 | private void extractReshuffledDiffElements(List<DifferenceElement> diffElements) { |
237 | for (int index = 0; index < diffElements.size(); index++) { |
238 | DifferenceElement diffElement = diffElements.get(index); |
239 | if (diffElement instanceof Reshuffled) { |
240 | Reshuffled reshuffled = (Reshuffled) diffElement; |
241 | |
242 | // First, let's see how many tokens we need to attribute to the previous version of the of the CsmMix |
243 | CsmMix elementsFromPreviousOrder = reshuffled.getPreviousOrder(); |
244 | CsmMix elementsFromNextOrder = reshuffled.getNextOrder(); |
245 | |
246 | // This contains indexes from elementsFromNextOrder to indexes from elementsFromPreviousOrder |
247 | Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = getCorrespondanceBetweenNextOrderAndPreviousOrder(elementsFromPreviousOrder, elementsFromNextOrder); |
248 | |
249 | // We now find out which Node Text elements corresponds to the elements in the original CSM |
250 | List<Integer> nodeTextIndexOfPreviousElements = findIndexOfCorrespondingNodeTextElement(elementsFromPreviousOrder.getElements(), nodeText, originalIndex, node); |
251 | |
252 | Map<Integer, Integer> nodeTextIndexToPreviousCSMIndex = new HashMap<>(); |
253 | for (int i = 0; i < nodeTextIndexOfPreviousElements.size(); i++) { |
254 | int value = nodeTextIndexOfPreviousElements.get(i); |
255 | if (value != -1) { |
256 | nodeTextIndexToPreviousCSMIndex.put(value, i); |
257 | } |
258 | } |
259 | int lastNodeTextIndex = nodeTextIndexOfPreviousElements.stream().max(Integer::compareTo).orElse(-1); |
260 | |
261 | // Elements to be added at the end |
262 | List<CsmElement> elementsToBeAddedAtTheEnd = new LinkedList<>(); |
263 | List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements(); |
264 | |
265 | Map<Integer, List<CsmElement>> elementsToAddBeforeGivenOriginalCSMElement = new HashMap<>(); |
266 | for (int ni = 0; ni < nextOrderElements.size(); ni++) { |
267 | // If it has a mapping, then it is kept |
268 | if (!correspondanceBetweenNextOrderAndPreviousOrder.containsKey(ni)) { |
269 | // Ok, it is something new. Where to put it? Let's see what is the first following |
270 | // element that has a mapping |
271 | int originalCsmIndex = -1; |
272 | for (int nj = ni + 1; nj < nextOrderElements.size() && originalCsmIndex == -1; nj++) { |
273 | if (correspondanceBetweenNextOrderAndPreviousOrder.containsKey(nj)) { |
274 | originalCsmIndex = correspondanceBetweenNextOrderAndPreviousOrder.get(nj); |
275 | if (!elementsToAddBeforeGivenOriginalCSMElement.containsKey(originalCsmIndex)) { |
276 | elementsToAddBeforeGivenOriginalCSMElement.put(originalCsmIndex, new LinkedList<>()); |
277 | } |
278 | elementsToAddBeforeGivenOriginalCSMElement.get(originalCsmIndex).add(nextOrderElements.get(ni)); |
279 | } |
280 | } |
281 | // it does not preceed anything, so it goes at the end |
282 | if (originalCsmIndex == -1) { |
283 | elementsToBeAddedAtTheEnd.add(nextOrderElements.get(ni)); |
284 | } |
285 | } |
286 | } |
287 | |
288 | // We go over the original node text elements, in the order they appear in the NodeText. |
289 | // Considering an original node text element (ONE) |
290 | // * we verify if it corresponds to a CSM element. If it does not we just move on, otherwise |
291 | // we find the correspond OCE (Original CSM Element) |
292 | // * we first add new elements that are marked to be added before OCE |
293 | // * if OCE is marked to be present also in the "after" CSM we add a kept element, |
294 | // otherwise we add a removed element |
295 | |
296 | // Remove the whole Reshuffled element |
297 | diffElements.remove(index); |
298 | |
299 | int diffElIterator = index; |
300 | if (lastNodeTextIndex != -1) { |
301 | for (int ntIndex = originalIndex; ntIndex <= lastNodeTextIndex; ntIndex++) { |
302 | |
303 | if (nodeTextIndexToPreviousCSMIndex.containsKey(ntIndex)) { |
304 | int indexOfOriginalCSMElement = nodeTextIndexToPreviousCSMIndex.get(ntIndex); |
305 | if (elementsToAddBeforeGivenOriginalCSMElement.containsKey(indexOfOriginalCSMElement)) { |
306 | for (CsmElement elementToAdd : elementsToAddBeforeGivenOriginalCSMElement.get(indexOfOriginalCSMElement)) { |
307 | diffElements.add(diffElIterator++, new Added(elementToAdd)); |
308 | } |
309 | } |
310 | |
311 | CsmElement originalCSMElement = elementsFromPreviousOrder.getElements().get(indexOfOriginalCSMElement); |
312 | boolean toBeKept = correspondanceBetweenNextOrderAndPreviousOrder.containsValue(indexOfOriginalCSMElement); |
313 | if (toBeKept) { |
314 | diffElements.add(diffElIterator++, new Kept(originalCSMElement)); |
315 | } else { |
316 | diffElements.add(diffElIterator++, new Removed(originalCSMElement)); |
317 | } |
318 | } |
319 | // else we have a simple node text element, without associated csm element, just keep ignore it |
320 | } |
321 | } |
322 | |
323 | // Finally we look for the remaining new elements that were not yet added and |
324 | // add all of them |
325 | for (CsmElement elementToAdd : elementsToBeAddedAtTheEnd) { |
326 | diffElements.add(diffElIterator++, new Added(elementToAdd)); |
327 | } |
328 | } |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * Maps all Removed elements as keys to their corresponding RemovedGroup. |
334 | * A RemovedGroup contains all consecutive Removed elements. |
335 | * <br> |
336 | * Example: |
337 | * <pre> |
338 | * Elements: Kept|Removed1|Removed2|Kept|Removed3|Added|Removed4 |
339 | * Groups: <----Group1----> Group2 Group3 |
340 | * Keys: Removed1+Removed2 Removed3 Removed4 |
341 | * </pre> |
342 | * |
343 | * @return Map with all Removed elements as keys to their corresponding RemovedGroup |
344 | */ |
345 | private Map<Removed, RemovedGroup> combineRemovedElementsToRemovedGroups() { |
346 | Map<Integer, List<Removed>> removedElementsMap = groupConsecutiveRemovedElements(); |
347 | |
348 | List<RemovedGroup> removedGroups = new ArrayList<>(); |
349 | for (Map.Entry<Integer, List<Removed>> entry : removedElementsMap.entrySet()) { |
350 | removedGroups.add(RemovedGroup.of(entry.getKey(), entry.getValue())); |
351 | } |
352 | |
353 | Map<Removed, RemovedGroup> map = new HashMap<>(); |
354 | for (RemovedGroup removedGroup : removedGroups){ |
355 | for (Removed index : removedGroup) { |
356 | map.put(index, removedGroup); |
357 | } |
358 | } |
359 | |
360 | return map; |
361 | } |
362 | |
363 | private Map<Integer, List<Removed>> groupConsecutiveRemovedElements() { |
364 | Map<Integer, List<Removed>> removedElementsMap = new HashMap<>(); |
365 | |
366 | Integer firstElement = null; |
367 | for (int i = 0; i < diffElements.size(); i++) { |
368 | DifferenceElement diffElement = diffElements.get(i); |
369 | if (diffElement instanceof Removed) { |
370 | if (firstElement == null) { |
371 | firstElement = i; |
372 | } |
373 | |
374 | removedElementsMap.computeIfAbsent(firstElement, key -> new ArrayList<>()) |
375 | .add((Removed) diffElement); |
376 | } else { |
377 | firstElement = null; |
378 | } |
379 | } |
380 | return removedElementsMap; |
381 | } |
382 | |
383 | private void applyRemovedDiffElement(RemovedGroup removedGroup, Removed removed, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) { |
384 | if (removed.isChild() && originalElementIsChild) { |
385 | ChildTextElement originalElementChild = (ChildTextElement) originalElement; |
386 | if (originalElementChild.isComment()) { |
387 | // We expected to remove a proper node but we found a comment in between. |
388 | // If the comment is associated to the node we want to remove we remove it as well, otherwise we keep it |
389 | Comment comment = (Comment) originalElementChild.getChild(); |
390 | if (!comment.isOrphan() && comment.getCommentedNode().isPresent() && comment.getCommentedNode().get().equals(removed.getChild())) { |
391 | nodeText.removeElement(originalIndex); |
392 | } else { |
393 | originalIndex++; |
394 | } |
395 | } else { |
396 | nodeText.removeElement(originalIndex); |
397 | |
398 | if ((diffIndex + 1 >= diffElements.size() || !(diffElements.get(diffIndex + 1) instanceof Added)) |
399 | && !removedGroup.isACompleteLine()) { |
400 | originalIndex = considerEnforcingIndentation(nodeText, originalIndex); |
401 | } |
402 | // If in front we have one space and before also we had space let's drop one space |
403 | if (originalElements.size() > originalIndex && originalIndex > 0) { |
404 | if (originalElements.get(originalIndex).isWhiteSpace() |
405 | && originalElements.get(originalIndex - 1).isWhiteSpace()) { |
406 | // However we do not want to do that when we are about to adding or removing elements |
407 | if ((diffIndex + 1) == diffElements.size() || (diffElements.get(diffIndex + 1) instanceof Kept)) { |
408 | originalElements.remove(originalIndex--); |
409 | } |
410 | } |
411 | } |
412 | |
413 | diffIndex++; |
414 | } |
415 | } else if (removed.isToken() && originalElementIsToken && |
416 | (removed.getTokenType() == ((TokenTextElement) originalElement).getTokenKind() |
417 | // handle EOLs separately as their token kind might not be equal. This is because the 'removed' |
418 | // element always has the current operating system's EOL as type |
419 | || (((TokenTextElement) originalElement).getToken().getCategory().isEndOfLine() |
420 | && removed.isNewLine()))) { |
421 | nodeText.removeElement(originalIndex); |
422 | diffIndex++; |
423 | } else if (originalElementIsToken && originalElement.isWhiteSpaceOrComment()) { |
424 | originalIndex++; |
425 | } else if (originalElement.isLiteral()) { |
426 | nodeText.removeElement(originalIndex); |
427 | diffIndex++; |
428 | } else if (removed.isPrimitiveType()) { |
429 | if (originalElement.isPrimitive()) { |
430 | nodeText.removeElement(originalIndex); |
431 | diffIndex++; |
432 | } else { |
433 | throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement); |
434 | } |
435 | } else if (removed.isWhiteSpace() || removed.getElement() instanceof CsmIndent || removed.getElement() instanceof CsmUnindent) { |
436 | diffIndex++; |
437 | } else if (originalElement.isWhiteSpace()) { |
438 | originalIndex++; |
439 | } else { |
440 | throw new UnsupportedOperationException("removed " + removed.getElement() + " vs " + originalElement); |
441 | } |
442 | |
443 | cleanTheLineOfLeftOverSpace(removedGroup, removed); |
444 | } |
445 | |
446 | /** |
447 | * Cleans the line of left over space if there is unnecessary indentation and the element will not be replaced |
448 | */ |
449 | private void cleanTheLineOfLeftOverSpace(RemovedGroup removedGroup, Removed removed) { |
450 | if (originalIndex >= originalElements.size()) { |
451 | // if all elements were already processed there is nothing to do |
452 | return; |
453 | } |
454 | |
455 | if (!removedGroup.isProcessed() |
456 | && removedGroup.getLastElement() == removed |
457 | && removedGroup.isACompleteLine()) { |
458 | Integer lastElementIndex = removedGroup.getLastElementIndex(); |
459 | Optional<Integer> indentation = removedGroup.getIndentation(); |
460 | |
461 | if (indentation.isPresent() && !isReplaced(lastElementIndex)) { |
462 | for (int i = 0; i < indentation.get(); i++) { |
463 | if (originalElements.get(originalIndex).isSpaceOrTab()) { |
464 | // If the current element is a space, remove it |
465 | nodeText.removeElement(originalIndex); |
466 | } else if (originalIndex >= 1 && originalElements.get(originalIndex - 1).isSpaceOrTab()) { |
467 | // If the current element is not a space itself we remove the space in front of it |
468 | nodeText.removeElement(originalIndex - 1); |
469 | originalIndex--; |
470 | } |
471 | } |
472 | } |
473 | |
474 | // Mark RemovedGroup as processed |
475 | removedGroup.processed(); |
476 | } |
477 | } |
478 | |
479 | // note: |
480 | // increment originalIndex if we want to keep the original element |
481 | // increment diffIndex if we don't want to skip the diff element |
482 | private void applyKeptDiffElement(Kept kept, TextElement originalElement, boolean originalElementIsChild, boolean originalElementIsToken) { |
483 | if (originalElement.isComment()) { |
484 | originalIndex++; |
485 | } else if (kept.isChild() && ((CsmChild)kept.getElement()).getChild() instanceof Comment ) { |
486 | diffIndex++; |
487 | } else if (kept.isChild() && originalElementIsChild) { |
488 | diffIndex++; |
489 | originalIndex++; |
490 | } else if (kept.isChild() && originalElementIsToken) { |
491 | if (originalElement.isWhiteSpaceOrComment()) { |
492 | originalIndex++; |
493 | } else if (originalElement.isIdentifier() && isNodeWithTypeArguments(kept)) { |
494 | diffIndex++; |
495 | // skip all token related to node with type argument declaration |
496 | // for example: |
497 | // List i : in this case originalElement is "List" and the next token is space. There is nothing to skip. in the originalElements list. |
498 | // List<String> i : in this case originalElement is "List" and the next token is |
499 | // "<" so we have to skip all the tokens which are used in the typed argument declaration [<][String][>](3 tokens) in the originalElements list. |
500 | // List<List<String>> i : in this case originalElement is "List" and the next |
501 | // token is "<" so we have to skip all the tokens which are used in the typed arguments declaration [<][List][<][String][>][>](6 tokens) in the originalElements list. |
502 | int step = getIndexToNextTokenElement((TokenTextElement) originalElement, 0); |
503 | originalIndex += step; |
504 | originalIndex++; |
505 | } else if (originalElement.isIdentifier()) { |
506 | originalIndex++; |
507 | diffIndex++; |
508 | } else { |
509 | if (kept.isPrimitiveType()) { |
510 | originalIndex++; |
511 | diffIndex++; |
512 | } else { |
513 | throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement); |
514 | } |
515 | } |
516 | } else if (kept.isToken() && originalElementIsToken) { |
517 | TokenTextElement originalTextToken = (TokenTextElement) originalElement; |
518 | |
519 | if (kept.getTokenType() == originalTextToken.getTokenKind()) { |
520 | originalIndex++; |
521 | diffIndex++; |
522 | } else if (kept.isNewLine() && originalTextToken.isNewline()) { |
523 | originalIndex++; |
524 | diffIndex++; |
525 | } else if (kept.isNewLine() && originalTextToken.isSpaceOrTab()) { |
526 | originalIndex++; |
527 | diffIndex++; |
528 | // case where originalTextToken is a separator like ";" and |
529 | // kept is not a new line or whitespace for example "}" |
530 | // see issue 2351 |
531 | } else if (!kept.isNewLine() && originalTextToken.isSeparator()) { |
532 | originalIndex++; |
533 | } else if (kept.isWhiteSpaceOrComment()) { |
534 | diffIndex++; |
535 | } else if (originalTextToken.isWhiteSpaceOrComment()) { |
536 | originalIndex++; |
537 | } else { |
538 | throw new UnsupportedOperationException("Csm token " + kept.getElement() + " NodeText TOKEN " + originalTextToken); |
539 | } |
540 | } else if (kept.isToken() && originalElementIsChild) { |
541 | diffIndex++; |
542 | } else if (kept.isWhiteSpace()) { |
543 | diffIndex++; |
544 | } else if (kept.isIndent()) { |
545 | diffIndex++; |
546 | } else if (kept.isUnindent()) { |
547 | // Nothing to do, beside considering indentation |
548 | // However we want to consider the case in which the indentation was not applied, like when we have |
549 | // just a left brace followed by space |
550 | |
551 | diffIndex++; |
552 | if (!openBraceWasOnSameLine()) { |
553 | for (int i = 0; i < STANDARD_INDENTATION_SIZE && originalIndex >= 1 && nodeText.getTextElement(originalIndex - 1).isSpaceOrTab(); i++) { |
554 | nodeText.removeElement(--originalIndex); |
555 | } |
556 | } |
557 | } else { |
558 | throw new UnsupportedOperationException("kept " + kept.getElement() + " vs " + originalElement); |
559 | } |
560 | } |
561 | |
562 | /* |
563 | * Returns true if the DifferenceElement is a CsmChild with type arguments |
564 | */ |
565 | private boolean isNodeWithTypeArguments(DifferenceElement element) { |
566 | CsmElement csmElem = element.getElement(); |
567 | if (!CsmChild.class.isAssignableFrom(csmElem.getClass())) |
568 | return false; |
569 | CsmChild child = (CsmChild) csmElem; |
570 | if (!NodeWithTypeArguments.class.isAssignableFrom(child.getChild().getClass())) |
571 | return false; |
572 | Optional<NodeList<Type>> typeArgs = ((NodeWithTypeArguments) child.getChild()).getTypeArguments(); |
573 | return typeArgs.isPresent() && typeArgs.get().size() > 0; |
574 | } |
575 | |
576 | /* |
577 | * Returns the number of tokens to skip in originalElements list to synchronize it with the DiffElements list |
578 | * This is due to the fact that types are considered as token in the originalElements list. |
579 | * For example, |
580 | * List<String> is represented by 4 tokens ([List][<][String][>]) while it's a CsmChild element in the DiffElements list |
581 | * So in this case, getIndexToNextTokenElement(..) on the [List] token returns 3 because we have to skip 3 tokens ([<][String][>]) to synchronize |
582 | * DiffElements list and originalElements list |
583 | * The end of recursivity is reached when there is no next token or if the nested diamond operators are totally managed, to take into account this type of declaration |
584 | * List <List<String>> l |
585 | * Be careful, this method must be call only if diamond operator could be found in the sequence |
586 | * |
587 | * @Param TokenTextElement the token currently analyzed |
588 | * @Param int the number of nested diamond operators |
589 | * @return the number of token to skip in originalElements list |
590 | */ |
591 | private int getIndexToNextTokenElement(TokenTextElement element, int nestedDiamondOperator) { |
592 | int step = 0; // number of token to skip |
593 | Optional<JavaToken> next = element.getToken().getNextToken(); |
594 | if (!next.isPresent()) return step; |
595 | // because there is a token, first we need to increment the number of token to skip |
596 | step++; |
597 | // manage nested diamond operators by incrementing the level on LT token and decrementing on GT |
598 | JavaToken token = next.get(); |
599 | Kind kind = Kind.valueOf(token.getKind()); |
600 | if (isDiamondOperator(kind)) { |
601 | if (kind.GT.equals(kind)) |
602 | nestedDiamondOperator--; |
603 | else |
604 | nestedDiamondOperator++; |
605 | } |
606 | // manage the fact where the first token is not a diamond operator but a whitespace |
607 | // and the end of the token sequence to skip |
608 | // for example in this declaration List <String> a; |
609 | if (nestedDiamondOperator == 0 && !next.get().getCategory().isWhitespace()) |
610 | return step; |
611 | // recursively analyze token to skip |
612 | return step += getIndexToNextTokenElement(new TokenTextElement(token), nestedDiamondOperator); |
613 | } |
614 | |
615 | /* |
616 | * Returns true if the token is possibly a diamond operator |
617 | */ |
618 | private boolean isDiamondOperator(Kind kind) { |
619 | return kind.GT.equals(kind) || kind.LT.equals(kind); |
620 | } |
621 | |
622 | private boolean openBraceWasOnSameLine() { |
623 | int index = originalIndex; |
624 | while (index >= 0 && !nodeText.getTextElement(index).isNewline()) { |
625 | if (nodeText.getTextElement(index).isToken(LBRACE)) { |
626 | return true; |
627 | } |
628 | index--; |
629 | } |
630 | return false; |
631 | } |
632 | |
633 | private boolean wasSpaceBetweenBraces() { |
634 | return nodeText.getTextElement(originalIndex).isToken(RBRACE) |
635 | && doWeHaveLeftBraceFollowedBySpace(originalIndex - 1) |
636 | && (diffIndex < 2 || !diffElements.get(diffIndex - 2).isRemoved()); |
637 | } |
638 | |
639 | private boolean doWeHaveLeftBraceFollowedBySpace(int index) { |
640 | index = rewindSpace(index); |
641 | return nodeText.getElements().get(index).isToken(LBRACE); |
642 | } |
643 | |
644 | private int rewindSpace(int index) { |
645 | if (index <= 0) { |
646 | return index; |
647 | } |
648 | if (nodeText.getElements().get(index).isWhiteSpace()) { |
649 | return rewindSpace(index - 1); |
650 | } else { |
651 | return index; |
652 | } |
653 | } |
654 | |
655 | private boolean nextIsRightBrace(int index) { |
656 | List<TextElement> elements = originalElements.subList(index, originalElements.size()); |
657 | for(TextElement element : elements) { |
658 | if (!element.isSpaceOrTab()) { |
659 | return element.isToken(RBRACE); |
660 | } |
661 | } |
662 | return false; |
663 | } |
664 | |
665 | private void applyAddedDiffElement(Added added) { |
666 | if (added.isIndent()) { |
667 | for (int i=0;i<STANDARD_INDENTATION_SIZE;i++){ |
668 | indentation.add(new TokenTextElement(GeneratedJavaParserConstants.SPACE)); |
669 | } |
670 | addedIndentation = true; |
671 | diffIndex++; |
672 | return; |
673 | } |
674 | if (added.isUnindent()) { |
675 | for (int i = 0; i<STANDARD_INDENTATION_SIZE && !indentation.isEmpty(); i++){ |
676 | indentation.remove(indentation.size() - 1); |
677 | } |
678 | addedIndentation = false; |
679 | diffIndex++; |
680 | return; |
681 | } |
682 | |
683 | TextElement addedTextElement = added.toTextElement(); |
684 | boolean used = false; |
685 | boolean isPreviousElementNewline = (originalIndex > 0) && originalElements.get(originalIndex - 1).isNewline(); |
686 | if (isPreviousElementNewline) { |
687 | List<TextElement> elements = processIndentation(indentation, originalElements.subList(0, originalIndex - 1)); |
688 | boolean nextIsRightBrace = nextIsRightBrace(originalIndex); |
689 | for (TextElement e : elements) { |
690 | if (!nextIsRightBrace |
691 | && e instanceof TokenTextElement |
692 | && originalElements.get(originalIndex).isToken(((TokenTextElement)e).getTokenKind())) { |
693 | originalIndex++; |
694 | } else { |
695 | nodeText.addElement(originalIndex++, e); |
696 | } |
697 | } |
698 | } else if (isAfterLBrace(nodeText, originalIndex) && !isAReplacement(diffIndex)) { |
699 | if (addedTextElement.isNewline()) { |
700 | used = true; |
701 | } |
702 | nodeText.addElement(originalIndex++, new TokenTextElement(TokenTypes.eolTokenKind())); |
703 | // This remove the space in "{ }" when adding a new line |
704 | while (originalIndex >= 2 && originalElements.get(originalIndex - 2).isSpaceOrTab()) { |
705 | originalElements.remove(originalIndex - 2); |
706 | originalIndex--; |
707 | } |
708 | for (TextElement e : processIndentation(indentation, originalElements.subList(0, originalIndex - 1))) { |
709 | nodeText.addElement(originalIndex++, e); |
710 | } |
711 | // Indentation is painful... |
712 | // Sometimes we want to force indentation: this is the case when indentation was expected but |
713 | // was actually not there. For example if we have "{ }" we would expect indentation but it is |
714 | // not there, so when adding new elements we force it. However if the indentation has been |
715 | // inserted by us in this transformation we do not want to insert it again |
716 | if (!addedIndentation) { |
717 | for (TextElement e : indentationBlock()) { |
718 | nodeText.addElement(originalIndex++, e); |
719 | } |
720 | } |
721 | } |
722 | |
723 | if (!used) { |
724 | // Handling trailing comments |
725 | boolean sufficientTokensRemainToSkip = nodeText.numberOfElements() > originalIndex + 2; |
726 | boolean currentIsAComment = nodeText.getTextElement(originalIndex).isComment(); |
727 | boolean previousIsAComment = originalIndex > 0 && nodeText.getTextElement(originalIndex - 1).isComment(); |
728 | boolean currentIsNewline = nodeText.getTextElement(originalIndex).isNewline(); |
729 | |
730 | if (sufficientTokensRemainToSkip && currentIsAComment) { |
731 | // Need to get behind the comment: |
732 | originalIndex += 2; // FIXME: Why 2? This comment and the next newline? |
733 | nodeText.addElement(originalIndex, addedTextElement); // Defer originalIndex increment |
734 | |
735 | // We want to adjust the indentation while considering the new element that we added |
736 | originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false); |
737 | originalIndex++; // Now we can increment |
738 | } else if (currentIsNewline && previousIsAComment) { |
739 | /* |
740 | * Manage the case where we want to add an element, after an expression which is followed by a comment on the same line. |
741 | * This is not the same case as the one who handles the trailing comments, because in this case the node text element is a new line (not a comment) |
742 | * For example : {@code private String a; // this is a } |
743 | */ |
744 | originalIndex++; // Insert after the new line which follows this comment. |
745 | |
746 | // We want to adjust the indentation while considering the new element that we added |
747 | originalIndex = adjustIndentation(indentation, nodeText, originalIndex, false); |
748 | nodeText.addElement(originalIndex, addedTextElement); // Defer originalIndex increment |
749 | |
750 | originalIndex++; // Now we can increment. |
751 | } else { |
752 | nodeText.addElement(originalIndex, addedTextElement); |
753 | originalIndex++; |
754 | } |
755 | } |
756 | |
757 | if (addedTextElement.isNewline()) { |
758 | boolean followedByUnindent = isFollowedByUnindent(diffElements, diffIndex); |
759 | boolean nextIsRightBrace = nextIsRightBrace(originalIndex); |
760 | boolean nextIsNewLine = nodeText.getTextElement(originalIndex).isNewline(); |
761 | if ((!nextIsNewLine && !nextIsRightBrace) || followedByUnindent) { |
762 | originalIndex = adjustIndentation(indentation, nodeText, originalIndex, followedByUnindent); |
763 | } |
764 | } |
765 | |
766 | diffIndex++; |
767 | } |
768 | |
769 | private String tokenDescription(int kind) { |
770 | return GeneratedJavaParserConstants.tokenImage[kind]; |
771 | } |
772 | |
773 | private Map<Integer, Integer> getCorrespondanceBetweenNextOrderAndPreviousOrder(CsmMix elementsFromPreviousOrder, CsmMix elementsFromNextOrder) { |
774 | Map<Integer, Integer> correspondanceBetweenNextOrderAndPreviousOrder = new HashMap<>(); |
775 | |
776 | List<CsmElement> nextOrderElements = elementsFromNextOrder.getElements(); |
777 | List<CsmElement> previousOrderElements = elementsFromPreviousOrder.getElements(); |
778 | WrappingRangeIterator piNext = new WrappingRangeIterator(previousOrderElements.size()); |
779 | |
780 | for (int ni = 0; ni < nextOrderElements.size(); ni++) { |
781 | boolean found = false; |
782 | CsmElement ne = nextOrderElements.get(ni); |
783 | |
784 | for (int counter = 0; counter < previousOrderElements.size() && !found; counter++) { |
785 | Integer pi = piNext.next(); |
786 | CsmElement pe = previousOrderElements.get(pi); |
787 | if (!correspondanceBetweenNextOrderAndPreviousOrder.values().contains(pi) |
788 | && DifferenceElementCalculator.matching(ne, pe)) { |
789 | found = true; |
790 | correspondanceBetweenNextOrderAndPreviousOrder.put(ni, pi); |
791 | } |
792 | } |
793 | } |
794 | |
795 | return correspondanceBetweenNextOrderAndPreviousOrder; |
796 | } |
797 | |
798 | private boolean isFollowedByUnindent(List<DifferenceElement> diffElements, int diffIndex) { |
799 | return (diffIndex + 1) < diffElements.size() |
800 | && diffElements.get(diffIndex + 1).isAdded() |
801 | && diffElements.get(diffIndex + 1).getElement() instanceof CsmUnindent; |
802 | } |
803 | |
804 | private List<Integer> findIndexOfCorrespondingNodeTextElement(List<CsmElement> elements, NodeText nodeText, int startIndex, Node node) { |
805 | List<Integer> correspondingIndices = new ArrayList<>(); |
806 | for (ListIterator<CsmElement> csmElementListIterator = elements.listIterator(); csmElementListIterator.hasNext(); ) { |
807 | |
808 | int previousCsmElementIndex = csmElementListIterator.previousIndex(); |
809 | CsmElement csmElement = csmElementListIterator.next(); |
810 | int nextCsmElementIndex = csmElementListIterator.nextIndex(); |
811 | |
812 | Map<MatchClassification, Integer> potentialMatches = new EnumMap<>(MatchClassification.class); |
813 | for (int i = startIndex; i < nodeText.getElements().size(); i++){ |
814 | if (!correspondingIndices.contains(i)) { |
815 | TextElement textElement = nodeText.getTextElement(i); |
816 | |
817 | boolean isCorresponding = isCorrespondingElement(textElement, csmElement, node); |
818 | |
819 | if (isCorresponding) { |
820 | boolean hasSamePreviousElement = false; |
821 | if (i > 0 && previousCsmElementIndex > -1) { |
822 | TextElement previousTextElement = nodeText.getTextElement(i - 1); |
823 | |
824 | hasSamePreviousElement = isCorrespondingElement(previousTextElement, elements.get(previousCsmElementIndex), node); |
825 | } |
826 | |
827 | boolean hasSameNextElement = false; |
828 | if (i < nodeText.getElements().size() - 1 && nextCsmElementIndex < elements.size()) { |
829 | TextElement nextTextElement = nodeText.getTextElement(i + 1); |
830 | |
831 | hasSameNextElement = isCorrespondingElement(nextTextElement, elements.get(nextCsmElementIndex), node); |
832 | } |
833 | |
834 | if (hasSamePreviousElement && hasSameNextElement) { |
835 | potentialMatches.putIfAbsent(MatchClassification.ALL, i); |
836 | } else if (hasSamePreviousElement) { |
837 | potentialMatches.putIfAbsent(MatchClassification.PREVIOUS_AND_SAME, i); |
838 | } else if (hasSameNextElement) { |
839 | potentialMatches.putIfAbsent(MatchClassification.NEXT_AND_SAME, i); |
840 | } else { |
841 | potentialMatches.putIfAbsent(MatchClassification.SAME_ONLY, i); |
842 | } |
843 | } else if (isAlmostCorrespondingElement(textElement, csmElement, node)) { |
844 | potentialMatches.putIfAbsent(MatchClassification.ALMOST, i); |
845 | } |
846 | } |
847 | } |
848 | |
849 | // Prioritize the matches from best to worst |
850 | Optional<MatchClassification> bestMatchKey = potentialMatches.keySet().stream() |
851 | .min(Comparator.comparing(MatchClassification::getPriority)); |
852 | |
853 | if (bestMatchKey.isPresent()) { |
854 | correspondingIndices.add(potentialMatches.get(bestMatchKey.get())); |
855 | } else { |
856 | correspondingIndices.add(-1); |
857 | } |
858 | } |
859 | |
860 | return correspondingIndices; |
861 | } |
862 | |
863 | private enum MatchClassification { |
864 | ALL(1), PREVIOUS_AND_SAME(2), NEXT_AND_SAME(3), SAME_ONLY(4), ALMOST(5); |
865 | |
866 | private final int priority; |
867 | |
868 | MatchClassification(int priority) { |
869 | this.priority = priority; |
870 | } |
871 | |
872 | int getPriority() { |
873 | return priority; |
874 | } |
875 | } |
876 | |
877 | private boolean isCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) { |
878 | if (csmElement instanceof CsmToken) { |
879 | CsmToken csmToken = (CsmToken)csmElement; |
880 | if (textElement instanceof TokenTextElement) { |
881 | TokenTextElement tokenTextElement = (TokenTextElement)textElement; |
882 | return tokenTextElement.getTokenKind() == csmToken.getTokenType() && tokenTextElement.getText().equals(csmToken.getContent(node)); |
883 | } |
884 | } else if (csmElement instanceof CsmChild) { |
885 | CsmChild csmChild = (CsmChild)csmElement; |
886 | if (textElement instanceof ChildTextElement) { |
887 | ChildTextElement childTextElement = (ChildTextElement)textElement; |
888 | return childTextElement.getChild() == csmChild.getChild(); |
889 | } |
890 | } else { |
891 | throw new UnsupportedOperationException(); |
892 | } |
893 | |
894 | return false; |
895 | } |
896 | |
897 | private boolean isAlmostCorrespondingElement(TextElement textElement, CsmElement csmElement, Node node) { |
898 | if (isCorrespondingElement(textElement, csmElement, node)) { |
899 | return false; |
900 | } |
901 | return textElement.isWhiteSpace() && csmElement instanceof CsmToken && ((CsmToken)csmElement).isWhiteSpace(); |
902 | } |
903 | |
904 | private int adjustIndentation(List<TokenTextElement> indentation, NodeText nodeText, int nodeTextIndex, boolean followedByUnindent) { |
905 | List<TextElement> indentationAdj = processIndentation(indentation, nodeText.getElements().subList(0, nodeTextIndex - 1)); |
906 | if (nodeTextIndex < nodeText.getElements().size() && nodeText.getElements().get(nodeTextIndex).isToken(RBRACE)) { |
907 | indentationAdj = indentationAdj.subList(0, indentationAdj.size() - Math.min(STANDARD_INDENTATION_SIZE, indentationAdj.size())); |
908 | } else if (followedByUnindent) { |
909 | indentationAdj = indentationAdj.subList(0, Math.max(0, indentationAdj.size() - STANDARD_INDENTATION_SIZE)); |
910 | } |
911 | for (TextElement e : indentationAdj) { |
912 | if ((nodeTextIndex< nodeText.getElements().size()) && nodeText.getElements().get(nodeTextIndex).isSpaceOrTab()) { |
913 | nodeTextIndex++; |
914 | } else { |
915 | nodeText.getElements().add(nodeTextIndex++, e); |
916 | } |
917 | } |
918 | if (nodeTextIndex < 0) { |
919 | throw new IllegalStateException(); |
920 | } |
921 | return nodeTextIndex; |
922 | } |
923 | |
924 | private boolean isAReplacement(int diffIndex) { |
925 | return (diffIndex > 0) && diffElements.get(diffIndex) instanceof Added && diffElements.get(diffIndex - 1) instanceof Removed; |
926 | } |
927 | |
928 | private boolean isReplaced(int diffIndex) { |
929 | return (diffIndex < diffElements.size() - 1) && diffElements.get(diffIndex + 1) instanceof Added && diffElements.get(diffIndex) instanceof Removed; |
930 | } |
931 | |
932 | @Override |
933 | public String toString() { |
934 | return "Difference{" + diffElements + '}'; |
935 | } |
936 | } |
937 |
Members