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

Camelcade / Perl5-IDEA / #525521563

01 Jun 2025 02:15PM UTC coverage: 82.332% (+0.04%) from 82.289%
#525521563

push

github

hurricup
Bounded wildcards

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

69 existing lines in 17 files now uncovered.

30882 of 37509 relevant lines covered (82.33%)

0.82 hits per line

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

89.05
/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✔
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<? super 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✔
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<? super 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✔
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✔
208
      return null;
×
209
    }
210
    if (StringUtil.isEmpty(newName)) {
1✔
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) {
228
    ASTNode result = node.getTreeNext();
×
229
    while (result != null && result.getElementType() == WHITE_SPACE) {
×
230
      result = result.getTreeNext();
×
231
    }
232
    return result;
×
233
  }
234

235
  @Contract("null -> null")
236
  public static @Nullable PsiElement getPrevSignificantSibling(@Nullable PsiElement element) {
237
    if (element == null) {
1✔
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

252
    Stub stub = currentElement instanceof StubBasedPsiElement<?> stubBasedElement ? stubBasedElement.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 {
258
      return PsiTreeUtil.getParentOfType(currentElement, psiClass);
×
259
    }
260
  }
261

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

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

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

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✔
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✔
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✔
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✔
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✔
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✔
376
          return false;
×
377
        }
378
      }
1✔
379
    }
380

381
    return true;
1✔
382
  }
383

384
  public static boolean iteratePsiElementsRight(PsiElement element, Processor<? super 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<? super 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<? super 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<? super 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<? super PerlLabelDeclaration> processor) {
474
    if (element == null) {
1✔
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✔
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✔
536
      return false;
×
537
    }
538

539
    PsiElement derefExpr = statement.getFirstChild();
1✔
540
    if (derefExpr == null) {
1✔
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<? extends PsiElement> targetElements,
566
                                        @NotNull List<? extends PsiElement> elementsToCompare) {
567
    if (targetElements.size() != elementsToCompare.size()) {
1✔
568
      return false;
1✔
569
    }
570
    for (int i = 0; i < targetElements.size(); i++) {
1✔
571
      if (!areElementsSame(targetElements.get(i), elementsToCompare.get(i))) {
1✔
572
        return false;
1✔
573
      }
574
    }
575
    return true;
1✔
576
  }
577

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

874
  public abstract static class HeredocProcessor implements Processor<PsiElement> {
875
    protected final int lineEndOffset;
876

877
    public HeredocProcessor(int lineEndOffset) {
1✔
878
      this.lineEndOffset = lineEndOffset;
1✔
879
    }
1✔
880
  }
881

882
  @SuppressWarnings("UnusedReturnValue")
883
  public static boolean processSubElements(@Nullable PsiElement rootElement,
884
                                           @NotNull PsiElementProcessor<? super PerlSubElement> processor) {
885
    StubElement<?> stubElement = getStubFromElement(rootElement);
1✔
886
    if (stubElement != null) {
1✔
887
      return processElementsFromStubs(stubElement, it -> !(it instanceof PerlSubElement subElement) || processor.execute(subElement), null);
1✔
888
    }
889
    return processElementsFromPsi(rootElement, it -> !(it instanceof PerlSubElement element) || processor.execute(element), null);
1✔
890
  }
891

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

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

911
    List<PsiElement> result = new ArrayList<>();
1✔
912
    for (PsiElement child : rawChildren) {
1✔
913
      ASTNode childNode = child.getNode();
1✔
914
      if (childNode instanceof LazyParseableElement) {
1✔
UNCOV
915
        result.addAll(getCompositeChildrenUnwrappingLazy(child));
×
916
      }
917
      else if (childNode instanceof CompositeElement) {
1✔
918
        result.add(child);
1✔
919
      }
920
    }
921

922
    return result;
1✔
923
  }
924

925
  /**
926
   * Looks for element of {@code clazz} in range from {@code startOffset} to the {@code endOffset}. Element should not start or end inside
927
   * bounds, but should be somewhere in this range.
928
   */
929
  @Contract(pure = true)
930
  public static @Nullable <T extends PsiElement> T findElementOfClassAtRange(@NotNull PsiFile file,
931
                                                                             int startOffset,
932
                                                                             int endOffset,
933
                                                                             @NotNull Class<T> clazz) {
934
    final FileViewProvider viewProvider = file.getViewProvider();
1✔
935
    for (Language lang : viewProvider.getLanguages()) {
1✔
936
      PsiElement run = viewProvider.findElementAt(startOffset, lang);
1✔
937
      while (run != null && run.getTextRange().getStartOffset() < endOffset) {
1✔
938
        var result = PsiTreeUtil.getParentOfType(run, clazz, false);
1✔
939
        if (result != null) {
1✔
940
          return result;
1✔
941
        }
942
        run = PsiTreeUtil.nextLeaf(run);
1✔
943
      }
1✔
944
    }
1✔
945
    return null;
1✔
946
  }
947
}
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