View Javadoc
1   /*
2    * MIT License
3    * Copyright (c) 2006-2025 JMockit developers
4    * See LICENSE file for full license text.
5    */
6   package mockit.coverage.reporting.parsing;
7   
8   import static java.util.Arrays.asList;
9   
10  import edu.umd.cs.findbugs.annotations.NonNull;
11  import edu.umd.cs.findbugs.annotations.Nullable;
12  
13  import java.util.Iterator;
14  import java.util.List;
15  import java.util.NoSuchElementException;
16  import java.util.regex.Pattern;
17  
18  public final class LineElement implements Iterable<LineElement> {
19      private static final List<String> CONDITIONAL_OPERATORS = asList("||", "&&", ":", "else", "?");
20      private static final Pattern OPEN_TAG = Pattern.compile("<");
21      private static final NoSuchElementException LAST_ELEMENT_REACHED = new NoSuchElementException();
22  
23      enum ElementType {
24          CODE, COMMENT, SEPARATOR
25      }
26  
27      enum ConditionalStatement {
28          IF("if"), FOR("for"), WHILE("while");
29  
30          @NonNull
31          private final String keyword;
32  
33          ConditionalStatement(@NonNull String keyword) {
34              this.keyword = keyword;
35          }
36  
37          @Nullable
38          static ConditionalStatement find(@NonNull String keyword) {
39              for (ConditionalStatement statement : values()) {
40                  if (statement.keyword.equals(keyword)) {
41                      return statement;
42                  }
43              }
44  
45              return null;
46          }
47      }
48  
49      @NonNull
50      private final ElementType type;
51      @NonNull
52      private final String text;
53      @Nullable
54      private String openingTag;
55      @Nullable
56      private String closingTag;
57      @Nullable
58      private LineElement next;
59  
60      @Nullable
61      private ConditionalStatement conditionalStatement;
62      private int parenthesesBalance;
63  
64      LineElement(@NonNull ElementType type, @NonNull String text) {
65          this.type = type;
66          this.text = OPEN_TAG.matcher(text).replaceAll("&lt;");
67      }
68  
69      public boolean isCode() {
70          return type == ElementType.CODE;
71      }
72  
73      public boolean isComment() {
74          return type == ElementType.COMMENT;
75      }
76  
77      public boolean isKeyword(@NonNull String keyword) {
78          return isCode() && text.equals(keyword);
79      }
80  
81      boolean isDotSeparator() {
82          return type == ElementType.SEPARATOR && text.charAt(0) == '.';
83      }
84  
85      @NonNull
86      public String getText() {
87          return text;
88      }
89  
90      @Nullable
91      public LineElement getNext() {
92          return next;
93      }
94  
95      void setNext(@Nullable LineElement next) {
96          this.next = next;
97      }
98  
99      @Nullable
100     public LineElement getNextCodeElement() {
101         if (next != null) {
102             for (LineElement element : next) {
103                 if (element.isCode()) {
104                     return element;
105                 }
106             }
107         }
108 
109         return null;
110     }
111 
112     public void wrapText(@NonNull String desiredOpeningTag, @NonNull String desiredClosingTag) {
113         openingTag = desiredOpeningTag;
114         closingTag = desiredClosingTag;
115     }
116 
117     @Nullable
118     public LineElement appendUntilNextCodeElement(@NonNull StringBuilder line) {
119         LineElement element = this;
120 
121         while (!element.isCode()) {
122             element.appendText(line);
123             element = element.next;
124 
125             if (element == null) {
126                 break;
127             }
128 
129             copyConditionalTrackingState(element);
130         }
131 
132         return element;
133     }
134 
135     private void copyConditionalTrackingState(@NonNull LineElement destination) {
136         destination.conditionalStatement = conditionalStatement;
137         destination.parenthesesBalance = parenthesesBalance;
138     }
139 
140     private void appendText(@NonNull StringBuilder line) {
141         if (openingTag == null) {
142             line.append(text);
143         } else {
144             line.append(openingTag).append(text).append(closingTag);
145         }
146     }
147 
148     @Nullable
149     public LineElement findNextBranchingPoint() {
150         if (conditionalStatement == null) {
151             conditionalStatement = ConditionalStatement.find(text);
152         }
153 
154         if (isBranchingElement()) {
155             if (next != null) {
156                 copyConditionalTrackingState(next);
157             }
158 
159             return this;
160         }
161 
162         if (conditionalStatement != null) {
163             int balance = getParenthesisBalance();
164             parenthesesBalance += balance;
165 
166             if (balance != 0 && parenthesesBalance == 0) {
167                 return next;
168             }
169         }
170 
171         if (next == null) {
172             return null;
173         }
174 
175         copyConditionalTrackingState(next);
176 
177         // noinspection TailRecursion
178         return next.findNextBranchingPoint();
179     }
180 
181     public boolean isBranchingElement() {
182         if (conditionalStatement == ConditionalStatement.FOR) {
183             int p = text.indexOf(':');
184 
185             if (p < 0) {
186                 p = text.indexOf(';');
187             }
188 
189             return p >= 0 && text.trim().length() == 1;
190         }
191 
192         return CONDITIONAL_OPERATORS.contains(text);
193     }
194 
195     private int getParenthesisBalance() {
196         int balance = 0;
197         int p = text.indexOf('(');
198 
199         while (p >= 0) {
200             balance++;
201             p = text.indexOf('(', p + 1);
202         }
203 
204         int q = text.indexOf(')');
205 
206         while (q >= 0) {
207             balance--;
208             q = text.indexOf(')', q + 1);
209         }
210 
211         return balance;
212     }
213 
214     @Nullable
215     public LineElement findWord(@NonNull String word) {
216         for (LineElement element : this) {
217             if (element.isCode() && word.equals(element.text)) {
218                 return element;
219             }
220         }
221 
222         return null;
223     }
224 
225     int getBraceBalanceUntilEndOfLine() {
226         int balance = 0;
227 
228         for (LineElement element : this) {
229             balance += element.getBraceBalance();
230         }
231 
232         return balance;
233     }
234 
235     private int getBraceBalance() {
236         if (isCode() && text.length() == 1) {
237             char c = text.charAt(0);
238 
239             if (c == '{') {
240                 return 1;
241             }
242             if (c == '}') {
243                 return -1;
244             }
245         }
246 
247         return 0;
248     }
249 
250     public void appendAllBefore(@NonNull StringBuilder line, @Nullable LineElement elementToStopBefore) {
251         LineElement elementToPrint = this;
252 
253         do {
254             elementToPrint.appendText(line);
255             elementToPrint = elementToPrint.next;
256         } while (elementToPrint != null && elementToPrint != elementToStopBefore);
257     }
258 
259     @NonNull
260     @Override
261     public Iterator<LineElement> iterator() {
262         return new Iterator<>() {
263             @Nullable
264             private LineElement current = LineElement.this;
265 
266             @Override
267             public boolean hasNext() {
268                 return current != null;
269             }
270 
271             @NonNull
272             @Override
273             public LineElement next() {
274                 if (current == null) {
275                     throw LAST_ELEMENT_REACHED;
276                 }
277 
278                 LineElement nextElement = current;
279                 current = current.next;
280 
281                 return nextElement;
282             }
283 
284             @Override
285             public void remove() {
286             }
287         };
288     }
289 
290     @Override
291     public String toString() {
292         StringBuilder line = new StringBuilder(200);
293 
294         for (LineElement element : this) {
295             element.appendText(line);
296         }
297 
298         return line.toString();
299     }
300 }