• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521519

21 Apr 2025 01:57PM UTC coverage: 82.17% (+0.01%) from 82.156%
#525521519

push

github

hurricup
CAMELCADE-22634 Cleanup

8 of 8 new or added lines in 2 files covered. (100.0%)

102 existing lines in 15 files now uncovered.

30868 of 37566 relevant lines covered (82.17%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.83
/plugin/core/src/main/java/com/perl5/lang/perl/psi/utils/PerlPsiUtil.java
1
/*
2
 * Copyright 2015-2025 Alexandr Evstigneev
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 * http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package com.perl5.lang.perl.psi.utils;
18

19
import com.intellij.extapi.psi.ASTWrapperPsiElement;
20
import com.intellij.lang.ASTNode;
21
import com.intellij.lang.Language;
22
import com.intellij.openapi.progress.ProgressManager;
23
import com.intellij.openapi.util.TextRange;
24
import com.intellij.openapi.util.text.StringUtil;
25
import com.intellij.psi.*;
26
import com.intellij.psi.impl.source.PsiFileImpl;
27
import com.intellij.psi.impl.source.tree.CompositeElement;
28
import com.intellij.psi.impl.source.tree.LazyParseableElement;
29
import com.intellij.psi.search.PsiElementProcessor;
30
import com.intellij.psi.stubs.PsiFileStub;
31
import com.intellij.psi.stubs.Stub;
32
import com.intellij.psi.stubs.StubBase;
33
import com.intellij.psi.stubs.StubElement;
34
import com.intellij.psi.tree.IElementType;
35
import com.intellij.psi.tree.TokenSet;
36
import com.intellij.psi.util.PsiTreeUtil;
37
import com.intellij.psi.util.PsiUtilCore;
38
import com.intellij.util.IncorrectOperationException;
39
import com.intellij.util.Processor;
40
import com.intellij.util.containers.ContainerUtil;
41
import com.perl5.lang.perl.lexer.PerlElementTypes;
42
import com.perl5.lang.perl.lexer.PerlTokenSetsEx;
43
import com.perl5.lang.perl.psi.*;
44
import com.perl5.lang.perl.psi.impl.*;
45
import com.perl5.lang.perl.psi.light.PerlDelegatingLightNamedElement;
46
import com.perl5.lang.perl.psi.mixins.PerlStatementMixin;
47
import com.perl5.lang.perl.psi.properties.PerlLabelScope;
48
import com.perl5.lang.perl.psi.properties.PerlLoop;
49
import com.perl5.lang.perl.psi.properties.PerlStatementsContainer;
50
import com.perl5.lang.perl.psi.stubs.PerlPolyNamedElementStub;
51
import com.perl5.lang.perl.util.PerlPackageUtil;
52
import org.jetbrains.annotations.Contract;
53
import org.jetbrains.annotations.NotNull;
54
import org.jetbrains.annotations.Nullable;
55

56
import java.util.ArrayList;
57
import java.util.Collection;
58
import java.util.Collections;
59
import java.util.List;
60

61
import static com.intellij.psi.TokenType.NEW_LINE_INDENT;
62
import static com.intellij.psi.TokenType.WHITE_SPACE;
63
import static com.perl5.lang.perl.lexer.PerlTokenSets.*;
64

65
public final class PerlPsiUtil implements PerlElementTypes {
66
  private PerlPsiUtil() {
67
  }
68

69
  private static final TokenSet IGNORE_WHEN_COMPARING = TokenSet.orSet(
1✔
70
    HEREDOC_BODIES_TOKENSET,
71
    HEREDOC_ENDS,
72
    VARIABLE_OPEN_BRACES,
73
    VARIABLE_CLOSE_BRACES,
74
    TokenSet.create(WHITE_SPACE, NEW_LINE_INDENT, COMMENT_LINE, COMMENT_ANNOTATION, SEMICOLON
1✔
75
    ));
76

77
  public static final char SINGLE_QUOTE_CHAR = '\'';
78
  public static final String SINGLE_QUOTE = "" + SINGLE_QUOTE_CHAR;
79
  public static final char DOUBLE_QUOTE_CHAR = '"';
80
  public static final String DOUBLE_QUOTE = "" + DOUBLE_QUOTE_CHAR;
81
  public static final char BACK_TICK_CHAR = '`';
82
  public static final String BACK_TICK = "" + BACK_TICK_CHAR;
83
  public static final String QUOTE_Q = "q";
84
  public static final String QUOTE_QQ = "qq";
85
  public static final String QUOTE_QX = "qx";
86

87
  /**
88
   * @return list of all meaningful children of the {@code psiElement} inside the {@code rangeInElement} skipping spaces, comments and so on
89
   */
90
  public static @NotNull List<PsiElement> getMeaningfulChildrenWithLeafs(@Nullable PsiElement psiElement) {
91
    return psiElement == null ? Collections.emptyList() :
1✔
92
           getMeaningfulChildrenWithLeafs(psiElement, TextRange.from(0, psiElement.getTextLength()));
1✔
93
  }
94

95
  /**
96
   * @return list of all meaningful children of the {@code psiElement} inside the {@code rangeInElement} skipping spaces, comments and so on
97
   */
98
  public static @NotNull List<PsiElement> getMeaningfulChildrenWithLeafs(@Nullable PsiElement psiElement,
99
                                                                         @Nullable TextRange rangeInElement) {
100
    if (psiElement == null || rangeInElement == null || rangeInElement.isEmpty()) {
1✔
UNCOV
101
      return Collections.emptyList();
×
102
    }
103
    List<PsiElement> result = new ArrayList<>();
1✔
104
    PsiElement run = psiElement.getFirstChild();
1✔
105
    while (run != null) {
1✔
106
      if (rangeInElement.contains(run.getTextRangeInParent())) {
1✔
107
        if (!IGNORE_WHEN_COMPARING.contains(PsiUtilCore.getElementType(run))) {
1✔
108
          result.add(run);
1✔
109
        }
110
      }
111
      else if (!result.isEmpty()) {
1✔
112
        return result;
1✔
113
      }
114
      run = run.getNextSibling();
1✔
115
    }
116

117
    return result;
1✔
118
  }
119

120
  /**
121
   * Recursively searches for string content elements beginning from specified PsiElement
122
   *
123
   * @param startWith PsiElement to start from (inclusive)
124
   * @return list of PerlStringContentElement
125
   */
126
  public static Collection<PerlStringContentElement> collectStringElements(@NotNull PsiElement startWith) {
127
    final List<PerlStringContentElement> result = new ArrayList<>();
1✔
128
    processStringElements(startWith, perlStringContentElement -> {
1✔
129
      result.add(perlStringContentElement);
1✔
130
      return true;
1✔
131
    });
132
    return result;
1✔
133
  }
134

135
  /**
136
   * Recursive searcher for string content elements
137
   *
138
   * @param startWith element to start with (inclusive)
139
   */
140
  public static void processStringElements(PsiElement startWith, Processor<PerlStringContentElement> processor) {
141
    while (startWith != null) {
1✔
142
      if (startWith instanceof PerlStringContentElement stringContentElement) {
1✔
143
        processor.process(stringContentElement);
1✔
144
      }
145

146
      if (startWith.getFirstChild() != null) {
1✔
147
        processStringElements(startWith.getFirstChild(), processor);
1✔
148
      }
149

150
      startWith = startWith.getNextSibling();
1✔
151
    }
152
  }
1✔
153

154
  public static @NotNull List<String> collectStringContents(PsiElement startWith) {
155
    if (startWith == null) {
1✔
UNCOV
156
      return Collections.emptyList();
×
157
    }
158

159
    List<String> result = new ArrayList<>();
1✔
160
    collectStringContentsRecursively(startWith, result);
1✔
161
    return result;
1✔
162
  }
163

164
  private static void collectStringContentsRecursively(PsiElement run, List<String> result) {
165
    while (run != null) {
1✔
166
      if (run instanceof PerlString) {
1✔
167
        result.add(ElementManipulators.getValueText(run));
1✔
168
      }
169
      else if (run instanceof PerlStringList stringList) {
1✔
170
        result.addAll(stringList.getStringContents());
1✔
171
      }
172
      else {
173
        if (run instanceof ASTWrapperPsiElement) {
1✔
174
          collectStringContentsRecursively(run.getFirstChild(), result);
1✔
175
        }
176
      }
177
      run = run.getNextSibling();
1✔
178
    }
179
  }
1✔
180

181
  /**
182
   * Searching for manipulator by element
183
   *
184
   * @param element PsiElement to search manipulator for
185
   * @return manipulator
186
   */
187
  public static @NotNull ElementManipulator<? super PsiElement> getManipulator(PsiElement element) {
188
    ElementManipulator<? super PsiElement> manipulator = ElementManipulators.getManipulator(element);
1✔
189
    if (manipulator == null) {
1✔
UNCOV
190
      throw new IncorrectOperationException("Unable to find manipulator for " + element.getClass().getName());
×
191
    }
192
    return manipulator;
1✔
193
  }
194

195

196
  public static @NotNull PsiElement renameNamedElement(@NotNull PsiNameIdentifierOwner nameIdentifierOwner, @Nullable String newName) {
197
    renameElement(nameIdentifierOwner.getNameIdentifier(), newName);
1✔
198
    return nameIdentifierOwner;
1✔
199
  }
200

201
  /**
202
   * Renaming PsiElement using manipulator
203
   */
204
  @SuppressWarnings("UnusedReturnValue")
205
  @Contract("null, _ -> null; !null, null -> !null")
206
  public static @Nullable PsiElement renameElement(@Nullable PsiElement element, @Nullable String newName) {
207
    if (element == null || !element.isValid()) {
1✔
UNCOV
208
      return null;
×
209
    }
210
    if (StringUtil.isEmpty(newName)) {
1✔
UNCOV
211
      return element;
×
212
    }
213
    return PerlPsiUtil.getManipulator(element).handleContentChange(element, newName);
1✔
214
  }
215

216

217
  public static @Nullable PsiElement getNextSignificantSibling(PsiElement element) {
218
    PsiElement result = element.getNextSibling();
1✔
219
    while (isCommentOrSpace(result)) {
1✔
220
      result = result.getNextSibling();
1✔
221
    }
222
    return result;
1✔
223
  }
224

225
  // this one is bad. What about comments and so on?
226
  @Deprecated
227
  public static @Nullable ASTNode getNextSignificantSibling(ASTNode node) {
UNCOV
228
    ASTNode result = node.getTreeNext();
×
229
    while (result != null && result.getElementType() == WHITE_SPACE) {
×
230
      result = result.getTreeNext();
×
231
    }
UNCOV
232
    return result;
×
233
  }
234

235
  @Contract("null -> null")
236
  public static @Nullable PsiElement getPrevSignificantSibling(@Nullable PsiElement element) {
237
    if (element == null) {
1✔
UNCOV
238
      return null;
×
239
    }
240
    PsiElement result = element.getPrevSibling();
1✔
241
    while (isCommentOrSpace(result)) {
1✔
242
      result = result.getPrevSibling();
1✔
243
    }
244
    return result;
1✔
245
  }
246

247
  public static @Nullable PsiElement getParentElementOrStub(PsiElement currentElement,
248
                                                            final @Nullable Class<? extends StubElement<?>> stubClass,
249
                                                            final @NotNull Class<? extends PsiElement> psiClass
250
  ) {
251

UNCOV
252
    Stub stub = currentElement instanceof StubBasedPsiElement ? ((StubBasedPsiElement<?>)currentElement).getStub() : null;
×
253
    if (stub != null && stubClass != null) {
×
254
      Stub parentStub = getParentStubOfType(stub, stubClass);
×
255
      return parentStub == null ? null : ((StubBase<?>)parentStub).getPsi();
×
256
    }
257
    else {
UNCOV
258
      return PsiTreeUtil.getParentOfType(currentElement, psiClass);
×
259
    }
260
  }
261

262
  public static @Nullable Stub getParentStubOfType(Stub currentStubElement, Class<? extends Stub> stubClass) {
263
    while (true) {
UNCOV
264
      if (currentStubElement == null) {
×
265
        return null;
×
266
      }
267

UNCOV
268
      if (stubClass.isInstance(currentStubElement)) {
×
269
        return currentStubElement;
×
270
      }
271

UNCOV
272
      if (currentStubElement instanceof PsiFileStub) {
×
273
        return null;
×
274
      }
275

UNCOV
276
      currentStubElement = currentStubElement.getParentStub();
×
277
    }
278
  }
279

280
  public static @NotNull List<PsiElement> collectNamespaceMembers(final @NotNull PsiElement rootElement,
281
                                                                  final @NotNull Class<? extends PsiElement> psiClass
282
  ) {
283
    final List<PsiElement> result = new ArrayList<>();
1✔
284

285
    Processor<PsiElement> processor = element -> {
1✔
286
      if (psiClass.isInstance(element)) {
1✔
287
        result.add(element);
1✔
288
      }
289
      return true;
1✔
290
    };
291

292
    if (rootElement instanceof StubBasedPsiElement<?> stubBasedPsiElement) {
1✔
293
      Stub rootElementStub = stubBasedPsiElement.getStub();
1✔
294

295
      if (rootElementStub != null) {
1✔
296
        processElementsFromStubs(
1✔
297
          rootElementStub,
298
          processor,
299
          PerlNamespaceDefinitionWithIdentifier.class
300
        );
301
        return result;
1✔
302
      }
303
    }
304

305
    processElementsFromPsi(
1✔
306
      rootElement,
307
      processor,
308
      PerlNamespaceDefinitionWithIdentifier.class
309
    );
310
    return result;
1✔
311
  }
312

313
  public static boolean processElementsFromStubs(
314
    @Nullable Stub stub,
315
    @Nullable Processor<? super PsiElement> processor,
316
    @Nullable Class<? extends PsiElement> avoidPsiClass
317
  ) {
318
    if (stub == null || processor == null) {
1✔
UNCOV
319
      return false;
×
320
    }
321

322
    for (Stub childStub : stub.getChildrenStubs()) {
1✔
323
      ProgressManager.checkCanceled();
1✔
324
      PsiElement childPsi = ((StubElement<?>)childStub).getPsi();
1✔
325
      if (!processor.process(childPsi)) {
1✔
326
        return false;
1✔
327
      }
328

329
      if (avoidPsiClass == null || !avoidPsiClass.isInstance(childPsi)) // don't enter sublcasses
1✔
330
      {
331
        if (!processElementsFromStubs(childStub, processor, avoidPsiClass)) {
1✔
UNCOV
332
          return false;
×
333
        }
334
      }
335
    }
1✔
336

337
    if (stub instanceof PerlPolyNamedElementStub<?> polyNamedElementStub) {
1✔
338
      for (PsiElement child : polyNamedElementStub.getPsi().getLightElements()) {
1✔
339
        ProgressManager.checkCanceled();
1✔
340
        if (!processor.process(child)) {
1✔
UNCOV
341
          return false;
×
342
        }
343
      }
1✔
344
    }
345

346
    return true;
1✔
347
  }
348

349

350
  public static boolean processElementsFromPsi(
351
    @Nullable PsiElement element,
352
    @Nullable Processor<? super PsiElement> processor,
353
    @Nullable Class<? extends PsiElement> avoidClass
354
  ) {
355
    if (element == null || processor == null) {
1✔
UNCOV
356
      return false;
×
357
    }
358

359
    for (PsiElement child : element.getChildren()) {
1✔
360
      ProgressManager.checkCanceled();
1✔
361
      if (!processor.process(child)) {
1✔
362
        return false;
1✔
363
      }
364

365
      if (avoidClass == null || !avoidClass.isInstance(child)) { // don't enter subclasses
1✔
366
        if (!processElementsFromPsi(child, processor, avoidClass)) {
1✔
UNCOV
367
          return false;
×
368
        }
369
      }
370
    }
371

372
    if (element instanceof PerlPolyNamedElement<?> perlPolyNamedElement) {
1✔
373
      for (PerlDelegatingLightNamedElement<?> lightNamedElement : perlPolyNamedElement.getLightElements()) {
1✔
374
        ProgressManager.checkCanceled();
1✔
375
        if (!processor.process(lightNamedElement)) {
1✔
UNCOV
376
          return false;
×
377
        }
378
      }
1✔
379
    }
380

381
    return true;
1✔
382
  }
383

384
  public static boolean iteratePsiElementsRight(PsiElement element, Processor<PsiElement> processor) {
385
    if (element == null || element instanceof PsiFile) {
1✔
386
      return false;
1✔
387
    }
388

389
    if (!processor.process(element)) {
1✔
390
      return false;
1✔
391
    }
392

393
    PsiElement run = element.getNextSibling();
1✔
394

395
    if (run == null) {
1✔
396
      return iteratePsiElementsRight(element.getParent(), processor);
1✔
397
    }
398

399
    return iteratePsiElementsRightDown(run, processor) && iteratePsiElementsRight(element.getParent(), processor);
1✔
400
  }
401

402
  public static boolean iteratePsiElementsRightDown(@NotNull PsiElement element, @NotNull Processor<PsiElement> processor) {
403
    boolean result = processor.process(element);
1✔
404
    if (result) {
1✔
405
      // checking children
406
      PsiElement run = element.getFirstChild();
1✔
407
      if (run != null) {
1✔
408
        result = iteratePsiElementsRightDown(run, processor);
1✔
409
      }
410

411
      // checking next sibling
412
      if (result) {
1✔
413
        run = element.getNextSibling();
1✔
414
        if (run != null) {
1✔
415
          result = iteratePsiElementsRightDown(run, processor);
1✔
416
        }
417
      }
418
    }
419
    return result;
1✔
420
  }
421

422
  public static boolean processNamespaceStatements(@NotNull PsiElement rootElement, Processor<PsiElement> processor) {
423
    PsiElement run = rootElement.getFirstChild();
1✔
424
    boolean result = true;
1✔
425
    while (run != null && result) {
1✔
426
      if (!(run instanceof PerlNamespaceDefinitionWithIdentifier)) {
1✔
427
        if (run instanceof PerlCompositeElement) {
1✔
428
          result = processor.process(run);
1✔
429
        }
430
        if (result && run instanceof PerlStatementsContainer) {
1✔
431
          result = processNamespaceStatements(run, processor);
1✔
432
        }
433
      }
434
      run = run.getNextSibling();
1✔
435
    }
436
    return result;
1✔
437
  }
438

439

440
  public static boolean isCommentOrSpace(PsiElement element) {
441
    return element != null && PerlTokenSetsEx.getMEANINGLESS_TOKENS().contains(PsiUtilCore.getElementType(element));
1✔
442
  }
443

444
  /**
445
   * Traversing tree according to next/last/redo labels resolution and processes all labels declarations
446
   *
447
   * @param element   element to start from
448
   * @param processor processor to process elements
449
   */
450
  public static void processNextRedoLastLabelDeclarations(PsiElement element, Processor<PerlLabelDeclaration> processor) {
451
    if (element == null || element instanceof PerlLabelScope) {
1✔
452
      return;
1✔
453
    }
454

455
    ProgressManager.checkCanceled();
1✔
456
    if (element instanceof PerlLoop) {
1✔
457
      PsiElement prevElement = getPrevSignificantSibling(element);
1✔
458
      if (prevElement instanceof PerlLabelDeclaration labelDeclaration) {
1✔
459
        if (!processor.process(labelDeclaration)) {
1✔
460
          return;
1✔
461
        }
462
      }
463
    }
464
    processNextRedoLastLabelDeclarations(element.getParent(), processor);
1✔
465
  }
1✔
466

467
  /**
468
   * Traversing tree according to goto labels resolution and processes all labels declarations
469
   *
470
   * @param element   element to start from
471
   * @param processor processor to process elements
472
   */
473
  public static void processGotoLabelDeclarations(PsiElement element, Processor<PerlLabelDeclaration> processor) {
474
    if (element == null) {
1✔
UNCOV
475
      return;
×
476
    }
477

478
    PsiElement run = element.getFirstChild();
1✔
479
    while (run != null) {
1✔
480
      ProgressManager.checkCanceled();
1✔
481
      if (run instanceof PerlLabelDeclaration perlLabelDeclaration) {
1✔
482
        if (!processor.process(perlLabelDeclaration)) {
1✔
483
          return;
1✔
484
        }
485
      }
486
      run = run.getNextSibling();
1✔
487
    }
488

489
    // next iteration
490
    if (!(element instanceof PsiFile)) {
1✔
491
      processGotoLabelDeclarations(element.getParent(), processor);
1✔
492
    }
493
  }
1✔
494

495
  /**
496
   * Checks if specified element is shift expression or $_[0] in the single statement of sub definition
497
   *
498
   * @param element element in question
499
   * @return check result
500
   */
501
  public static boolean isSelfShortcut(PsiElement element) {
502
    if (element instanceof PsiPerlParenthesisedExpr parenthesisedExpr) {
1✔
503
      return isSelfShortcut(parenthesisedExpr.getExpr());
1✔
504
    }
505

506
    if (element == null ||
1✔
507
        !(element instanceof PsiPerlArrayShiftExpr ||
508
          element instanceof PsiPerlArrayElementImpl && StringUtil.equals(element.getText(), "$_[0]")
1✔
509
        )) {
510
      return false;
1✔
511
    }
512

513
    PsiPerlStatement statement = PsiTreeUtil.getParentOfType(element, PsiPerlStatementImpl.class);
1✔
514
    if (statement == null) {
1✔
UNCOV
515
      return false;
×
516
    }
517

518
    PsiElement statementContainer = statement.getParent();
1✔
519
    if (!(statementContainer instanceof PsiPerlBlockImpl perlBlock &&
1✔
520
          perlBlock.getContainer() instanceof PerlSubDefinitionElement)) {
1✔
521
      return false;
1✔
522
    }
523

524
    PsiPerlStatement[] statements = PsiTreeUtil.getChildrenOfType(statementContainer, PsiPerlStatement.class);
1✔
525
    return statements != null && statements.length == 1;
1✔
526
  }
527

528
  /**
529
   * Checks if statement is shift/$_[0] deref shortcut
530
   *
531
   * @param statement statement
532
   * @return check result
533
   */
534
  public static boolean isSelfShortcutStatement(PsiPerlStatement statement) {
535
    if (statement == null) {
1✔
UNCOV
536
      return false;
×
537
    }
538

539
    PsiElement derefExpr = statement.getFirstChild();
1✔
540
    if (derefExpr == null) {
1✔
UNCOV
541
      return false;
×
542
    }
543

544
    if (derefExpr instanceof PsiPerlReturnExprImpl) {
1✔
545
      derefExpr = derefExpr.getLastChild();
1✔
546
    }
547

548
    if (!(derefExpr instanceof PsiPerlDerefExpr)) {
1✔
549
      return false;
1✔
550
    }
551

552
    return isSelfShortcut(derefExpr.getFirstChild());
1✔
553
  }
554

555
  public static @NotNull PsiElement getClosest(@NotNull PsiElement existingElement, @Nullable PsiElement possibleElement) {
556
    if (possibleElement == null) {
1✔
557
      return existingElement;
1✔
558
    }
559
    return PsiTreeUtil.isAncestor(existingElement, possibleElement, true) ? possibleElement : existingElement;
1✔
560
  }
561

562
  /**
563
   * True iff all elements from provided lists are semantically equals
564
   */
565
  public static boolean areElementsSame(@NotNull List<PsiElement> targetElements, @NotNull List<PsiElement> elementsToCompare) {
566
    if (targetElements.size() != elementsToCompare.size()) {
1✔
567
      return false;
1✔
568
    }
569
    for (int i = 0; i < targetElements.size(); i++) {
1✔
570
      if (!areElementsSame(targetElements.get(i), elementsToCompare.get(i))) {
1✔
571
        return false;
1✔
572
      }
573
    }
574
    return true;
1✔
575
  }
576

577
  /**
578
   * @return true iff {@code elementToCompare} is equal to {@code targetElement} or both nulls
579
   */
580
  public static boolean areElementsSame(@Nullable PsiElement targetElement, @Nullable PsiElement elementToCompare) {
581
    if (targetElement == null && elementToCompare == null) {
1✔
UNCOV
582
      return true;
×
583
    }
584
    else if (targetElement == null || elementToCompare == null) {
1✔
UNCOV
585
      return false;
×
586
    }
587
    else if (targetElement.equals(elementToCompare)) {
1✔
588
      return true;
1✔
589
    }
590

591
    return switch (targetElement) {
1✔
592
      case PerlString string -> areStringElementsSame(string, elementToCompare);
1✔
593
      case PerlSimpleRegex regex -> areMatchRegexElementsSame(regex, elementToCompare);
1✔
594
      case PerlCastExpression expression -> areCastElementsSame(expression, elementToCompare);
1✔
595
      case PsiPerlPerlRegex regex -> areRegexSame(regex, elementToCompare);
1✔
596
      case PerlMethodContainer container -> areNestedCallsSame(container, elementToCompare);
1✔
597
      case PsiPerlPackageExpr expr -> elementToCompare instanceof PerlString ? areElementsSame(elementToCompare, expr) :
1✔
598
        elementToCompare instanceof PsiPerlPackageExpr &&
1✔
599
        StringUtil.equals(PerlPackageUtil.getCanonicalName(expr.getText()),
1✔
600
                          PerlPackageUtil.getCanonicalName(elementToCompare.getText()));
1✔
601
      default -> areGenericElementsSame(targetElement, elementToCompare);
1✔
602
    };
603
  }
604

605
  /**
606
   * @return true iff {@code element} is matching part of replace regex
607
   */
608
  public static boolean isMatchRegex(@NotNull PsiElement element) {
609
    if (!(element instanceof PsiPerlPerlRegex)) {
1✔
610
      return false;
1✔
611
    }
612

613
    PsiElement elementParent = element.getParent();
1✔
614
    return elementParent instanceof PerlReplacementRegex && element.equals(((PerlReplacementRegex)elementParent).getMatchRegex());
1✔
615
  }
616

617
  /**
618
   * @return true iff nested calls are semantically equal
619
   */
620
  private static boolean areNestedCallsSame(@NotNull PerlMethodContainer targetElement, @NotNull PsiElement elementToCompare) {
621
    if (!(elementToCompare instanceof PerlMethodContainer methodContainer)) {
1✔
622
      return false;
1✔
623
    }
624
    if (!areElementsSame(targetElement.getMethod(), methodContainer.getMethod())) {
1✔
625
      return false;
1✔
626
    }
627
    return areElementsSame(targetElement.getCallArgumentsList(), methodContainer.getCallArgumentsList());
1✔
628
  }
629

630
  /**
631
   * @return true iff regexps are semantically equal
632
   */
633
  private static boolean areRegexSame(@NotNull PsiPerlPerlRegex targetElement, @NotNull PsiElement elementTocompare) {
634
    if (elementTocompare instanceof PerlSimpleRegex simpleRegex) {
1✔
635
      return areElementsSame(targetElement, simpleRegex.getRegex());
1✔
636
    }
637
    if (!(elementTocompare instanceof PsiPerlPerlRegex)) {
1✔
638
      return false;
1✔
639
    }
640
    PsiElement targetElementRun = targetElement.getFirstChild();
1✔
641
    PsiElement runToCompare = elementTocompare.getFirstChild();
1✔
642
    while (true) {
643
      StringBuilder targetBuffer = new StringBuilder();
1✔
644
      StringBuilder bufferToCompare = new StringBuilder();
1✔
645
      //noinspection Duplicates
646
      while (targetElementRun != null) {
1✔
647
        IElementType targetElementType = PsiUtilCore.getElementType(targetElementRun);
1✔
648
        if (targetElementType == REGEX_TOKEN) {
1✔
649
          targetBuffer.append(targetElementRun.getText());
1✔
650
        }
651
        else if (!IGNORE_WHEN_COMPARING.contains(targetElementType)) {
1✔
652
          break;
1✔
653
        }
654
        targetElementRun = targetElementRun.getNextSibling();
1✔
655
      }
1✔
656

657
      //noinspection Duplicates
658
      while (runToCompare != null) {
1✔
659
        IElementType elementToCompareType = PsiUtilCore.getElementType(runToCompare);
1✔
660
        if (elementToCompareType == REGEX_TOKEN) {
1✔
661
          bufferToCompare.append(runToCompare.getText());
1✔
662
        }
663
        else if (!IGNORE_WHEN_COMPARING.contains(elementToCompareType)) {
1✔
664
          break;
1✔
665
        }
666
        runToCompare = runToCompare.getNextSibling();
1✔
667
      }
1✔
668

669
      if (targetElementRun == null && runToCompare == null) {
1✔
670
        return StringUtil.equals(targetBuffer, bufferToCompare);
1✔
671
      }
672
      if (targetElementRun == null || runToCompare == null ||
1✔
673
          !StringUtil.equals(targetBuffer, bufferToCompare) ||
1✔
674
          !areElementsSame(targetElementRun, runToCompare)) {
1✔
675
        return false;
1✔
676
      }
677
      targetElementRun = targetElementRun.getNextSibling();
1✔
678
      runToCompare = runToCompare.getNextSibling();
1✔
679
    }
1✔
680
  }
681

682
  /**
683
   * @return true iff {@code targetElement} and {@code elementToCompare} are semantically equal simple regexp elements
684
   */
685
  private static boolean areMatchRegexElementsSame(@NotNull PerlSimpleRegex targetElement, @NotNull PsiElement elementToCompare) {
686
    if (isMatchRegex(elementToCompare)) {
1✔
687
      return areElementsSame(targetElement.getRegex(), elementToCompare);
1✔
688
    }
689
    if (!(elementToCompare instanceof PerlSimpleRegex simpleRegex)) {
1✔
690
      return false;
1✔
691
    }
692
    if (!areElementsSame(targetElement.getRegex(), simpleRegex.getRegex())) {
1✔
693
      return false;
1✔
694
    }
695
    PerlRegexpModifiers targetModifiers = PerlRegexpModifiers.create(targetElement.getPerlRegexModifiers());
1✔
696
    PerlRegexpModifiers elementModifiers = PerlRegexpModifiers.create(simpleRegex.getPerlRegexModifiers());
1✔
697
    return targetModifiers == null && elementModifiers == null ||
1✔
698
           targetModifiers != null && targetModifiers.areRegexpComparable(elementModifiers);
1✔
699
  }
700

701
  /**
702
   * @return true iff cast (deref) {@code targetElement} semantically equals to {@code elementToCompare}. Taking into account blocked and non-blocked versions
703
   */
704
  private static boolean areCastElementsSame(@NotNull PerlCastExpression targetElement, @NotNull PsiElement elementToCompare) {
705
    if (!targetElement.getClass().equals(elementToCompare.getClass())) {
1✔
706
      return false;
1✔
707
    }
708
    PsiPerlExpr targetExpr = targetElement.getExpr();
1✔
709
    PsiPerlExpr exprToCompare = ((PerlCastExpression)elementToCompare).getExpr();
1✔
710
    if (targetExpr != null && exprToCompare != null) {
1✔
711
      return areElementsSame(targetExpr, exprToCompare);
1✔
712
    }
713

714
    PsiPerlBlock targetBlock = targetElement.getBlock();
1✔
715
    PsiPerlBlock blockToCompare = ((PerlCastExpression)elementToCompare).getBlock();
1✔
716
    if (targetBlock != null && blockToCompare != null) {
1✔
717
      return areElementsSame(targetBlock, blockToCompare);
1✔
718
    }
719
    return targetBlock == null ?
1✔
720
           areElementsSame(targetExpr, getSingleBlockExpression(blockToCompare)) :
1✔
721
           areElementsSame(getSingleBlockExpression(targetBlock), exprToCompare);
1✔
722
  }
723

724
  /**
725
   * @return if {@code block} contains only single statement with single expression, returns it. False otherwise.
726
   */
727
  public static @Nullable PsiPerlExpr getSingleBlockExpression(@Nullable PsiPerlBlock block) {
728
    if (block == null) {
1✔
UNCOV
729
      return null;
×
730
    }
731
    PsiElement[] children = block.getChildren();
1✔
732
    if (children.length != 1 || !(children[0] instanceof PerlStatementMixin statementMixin) || statementMixin.getModifier() != null) {
1✔
733
      return null;
1✔
734
    }
735
    return statementMixin.getExpr();
1✔
736
  }
737

738
  /**
739
   * @return true iff {@code targetString} is non-qx string and represents the same string as {@code elementToCompare}
740
   */
741
  private static boolean areStringElementsSame(@NotNull PerlString targetString, @NotNull PsiElement elementToCompare) {
742
    boolean isTargetExecutable = targetString instanceof PsiPerlStringXq;
1✔
743
    if (!isTargetExecutable && elementToCompare instanceof PsiPerlPackageExpr && targetString.getChildren().length == 0) {
1✔
744
      return StringUtil.equals(PerlPackageUtil.getCanonicalName(ElementManipulators.getValueText(targetString)),
1✔
745
                               PerlPackageUtil.getCanonicalName(elementToCompare.getText()));
1✔
746
    }
747
    if ((isTargetExecutable || elementToCompare instanceof PsiPerlStringXq)
1✔
748
        && !targetString.getClass().equals(elementToCompare.getClass())) {
1✔
749
      return false;
1✔
750
    }
751
    if (!(elementToCompare instanceof PerlString perlString)) {
1✔
752
      return false;
1✔
753
    }
754

755
    // textual comparison for simple strings
756
    boolean isTargetSimple = targetString.getChildren().length == 0;
1✔
757
    boolean isElementSimple = elementToCompare.getChildren().length == 0;
1✔
758
    if (isTargetSimple && isElementSimple) {
1✔
759
      String targetText = ElementManipulators.getValueText(targetString);
1✔
760
      String textToCompare = ElementManipulators.getValueText(elementToCompare);
1✔
761
      if (StringUtil.equals(targetText, textToCompare)) {
1✔
762
        return true;
1✔
763
      }
764
      else if (isTargetExecutable) {
1✔
765
        return false;
1✔
766
      }
767
      else if (targetString.getPrevSibling() == null && targetString.getParent() instanceof PerlDerefExpression ||
1✔
768
               elementToCompare.getPrevSibling() == null && elementToCompare.getParent() instanceof PerlDerefExpression) {
1✔
769
        return StringUtil.equals(PerlPackageUtil.getCanonicalName(targetText), PerlPackageUtil.getCanonicalName(textToCompare));
1✔
770
      }
771
      return false;
1✔
772
    }
773
    else if (isTargetSimple != isElementSimple) {
1✔
774
      return false;
1✔
775
    }
776

777
    // traversal comparison
778
    PsiElement targetRun = targetString.getFirstContentToken();
1✔
779
    PsiElement runToCompare = perlString.getFirstContentToken();
1✔
780
    PsiElement targetCloseQuoteElement = targetString.getCloseQuoteElement();
1✔
781
    PsiElement elementToCompareCloseQuoteElement = perlString.getCloseQuoteElement();
1✔
782

783
    while (targetRun != null && runToCompare != null) {
1✔
784
      if (targetRun.equals(targetCloseQuoteElement) && runToCompare.equals(elementToCompareCloseQuoteElement)) {
1✔
785
        return true;
1✔
786
      }
787

788
      if (!areElementsSame(targetRun, runToCompare)) {
1✔
UNCOV
789
        return false;
×
790
      }
791
      targetRun = targetRun.getNextSibling();
1✔
792
      runToCompare = runToCompare.getNextSibling();
1✔
793
    }
794

UNCOV
795
    return (targetRun == null || targetRun.equals(targetCloseQuoteElement)) &&
×
796
           (runToCompare == null || runToCompare.equals(elementToCompareCloseQuoteElement));
×
797
  }
798

799
  /**
800
   * @return true iff {@code targetElement} equals {@code elementToCompare} in generic way (same tree)
801
   */
802
  private static boolean areGenericElementsSame(@NotNull PsiElement targetElement, @NotNull PsiElement elementToCompare) {
803
    if (!targetElement.getClass().equals(elementToCompare.getClass())) {
1✔
804
      return false;
1✔
805
    }
806
    PsiElement targetElementRun = targetElement.getFirstChild();
1✔
807
    PsiElement elementToCompareRun = elementToCompare.getFirstChild();
1✔
808
    if (targetElementRun == null) {
1✔
809
      return elementToCompareRun == null &&
1✔
810
             StringUtil.equals(targetElement.getNode().getChars(), elementToCompare.getNode().getChars());
1✔
811
    }
812
    while (true) {
813
      while (IGNORE_WHEN_COMPARING.contains(PsiUtilCore.getElementType(targetElementRun))) {
1✔
814
        targetElementRun = targetElementRun.getNextSibling();
1✔
815
      }
816
      while (IGNORE_WHEN_COMPARING.contains(PsiUtilCore.getElementType(elementToCompareRun))) {
1✔
817
        elementToCompareRun = elementToCompareRun.getNextSibling();
1✔
818
      }
819
      if (targetElementRun == null || elementToCompareRun == null) {
1✔
UNCOV
820
        break;
×
821
      }
822

823
      if (!areElementsSame(targetElementRun, elementToCompareRun)) {
1✔
824
        return false;
1✔
825
      }
826

827
      targetElementRun = targetElementRun.getNextSibling();
1✔
828
      elementToCompareRun = elementToCompareRun.getNextSibling();
1✔
829
    }
830
    return targetElementRun == null && elementToCompareRun == null;
1✔
831
  }
832

833
  /**
834
   * @return true iff element is inside the qw list
835
   */
836
  public static boolean isInStringList(@NotNull PsiElement element) {
UNCOV
837
    return PsiUtilCore.getElementType(element.getParent()) == STRING_LIST;
×
838
  }
839

840
  /**
841
   * @return a single quoted string with content. Attempts to select proper quotes
842
   */
843
  public static @NotNull String createSingleQuotedString(@NotNull String content) {
844
    char openQuoteChar = PerlString.suggestOpenQuoteChar(content, SINGLE_QUOTE_CHAR);
1✔
845
    return openQuoteChar == 0 ? SINGLE_QUOTE + StringUtil.escapeChar(content, SINGLE_QUOTE_CHAR) + SINGLE_QUOTE :
1✔
846
           openQuoteChar == SINGLE_QUOTE_CHAR ? SINGLE_QUOTE + content + SINGLE_QUOTE :
1✔
847
           PerlPsiUtil.QUOTE_Q + openQuoteChar + content + PerlString.getQuoteCloseChar(openQuoteChar);
1✔
848
  }
849

850
  /**
851
   * Tries to get stub from passed {@code element} if available
852
   */
853
  @Contract("null->null")
854
  public static @Nullable StubElement<?> getStubFromElement(@Nullable PsiElement element) {
855
    StubElement<?> stubElement = null;
1✔
856
    if (element instanceof PsiFileImpl psiFile) {
1✔
857
      stubElement = psiFile.getStub();
1✔
858
    }
859
    else if (element instanceof StubBasedPsiElement<?> stubBasedPsiElement) {
1✔
860
      stubElement = stubBasedPsiElement.getStub();
1✔
861
    }
862
    return stubElement;
1✔
863
  }
864

865
  public static @NotNull String dumpHierarchy(@Nullable PsiElement element) {
866
    if (element == null) {
1✔
UNCOV
867
      return "null";
×
868
    }
869
    StringBuilder sb = new StringBuilder();
1✔
870
    while (true) {
871
      sb.append(element.getClass()).append("(").append(PsiUtilCore.getElementType(element)).append(")").append(": ");
1✔
872
      if (element instanceof PsiFile || element.getParent() == null) {
1✔
UNCOV
873
        break;
×
874
      }
875
      element = element.getParent();
1✔
876
    }
877
    return sb.toString();
1✔
878
  }
879

880
  public abstract static class HeredocProcessor implements Processor<PsiElement> {
881
    protected final int lineEndOffset;
882

883
    public HeredocProcessor(int lineEndOffset) {
1✔
884
      this.lineEndOffset = lineEndOffset;
1✔
885
    }
1✔
886
  }
887

888
  @SuppressWarnings("UnusedReturnValue")
889
  public static boolean processSubElements(@Nullable PsiElement rootElement, @NotNull PsiElementProcessor<PerlSubElement> processor) {
890
    StubElement<?> stubElement = getStubFromElement(rootElement);
1✔
891
    if (stubElement != null) {
1✔
892
      return processElementsFromStubs(stubElement, it -> !(it instanceof PerlSubElement subElement) || processor.execute(subElement), null);
1✔
893
    }
894
    return processElementsFromPsi(rootElement, it -> !(it instanceof PerlSubElement) || processor.execute((PerlSubElement)it), null);
1✔
895
  }
896

897
  /**
898
   * @return removes meaningless elements from the {@code source} list
899
   */
900
  public static @NotNull <T extends PsiElement> List<T> cleanupChildren(@NotNull T[] source) {
901
    return ContainerUtil.filter(source, it -> !PerlTokenSetsEx.getWHITE_SPACE_AND_COMMENTS().contains(PsiUtilCore.getElementType(it)));
1✔
902
  }
903

904
  /**
905
   * @return composite children of the given {@code psiElement}. If child happens to be lazy parsable, it's children collected instead of it.
906
   */
907
  public static @NotNull List<? extends PsiElement> getCompositeChildrenUnwrappingLazy(@Nullable PsiElement psiElement) {
908
    if (psiElement == null) {
1✔
UNCOV
909
      return Collections.emptyList();
×
910
    }
911
    @NotNull PsiElement[] rawChildren = psiElement.getChildren();
1✔
912
    if (rawChildren.length == 0) {
1✔
UNCOV
913
      return Collections.emptyList();
×
914
    }
915

916
    List<PsiElement> result = new ArrayList<>();
1✔
917
    for (PsiElement child : rawChildren) {
1✔
918
      ASTNode childNode = child.getNode();
1✔
919
      if (childNode instanceof LazyParseableElement) {
1✔
UNCOV
920
        result.addAll(getCompositeChildrenUnwrappingLazy(child));
×
921
      }
922
      else if (childNode instanceof CompositeElement) {
1✔
923
        result.add(child);
1✔
924
      }
925
    }
926

927
    return result;
1✔
928
  }
929

930
  /**
931
   * Looks for element of {@code clazz} in range from {@code startOffset} to the {@code endOffset}. Element should not start or end inside
932
   * bounds, but should be somewhere in this range.
933
   */
934
  @Contract(pure = true)
935
  public static @Nullable <T extends PsiElement> T findElementOfClassAtRange(@NotNull PsiFile file,
936
                                                                             int startOffset,
937
                                                                             int endOffset,
938
                                                                             @NotNull Class<T> clazz) {
939
    final FileViewProvider viewProvider = file.getViewProvider();
1✔
940
    for (Language lang : viewProvider.getLanguages()) {
1✔
941
      PsiElement run = viewProvider.findElementAt(startOffset, lang);
1✔
942
      while (run != null && run.getTextRange().getStartOffset() < endOffset) {
1✔
943
        var result = PsiTreeUtil.getParentOfType(run, clazz, false);
1✔
944
        if (result != null) {
1✔
945
          return result;
1✔
946
        }
947
        run = PsiTreeUtil.nextLeaf(run);
1✔
948
      }
1✔
949
    }
1✔
950
    return null;
1✔
951
  }
952
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc