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

Camelcade / Perl5-IDEA / #525521660

24 Aug 2025 01:28PM UTC coverage: 75.89% (-6.3%) from 82.227%
#525521660

push

github

hurricup
Migrated coverage reporting to https://github.com/nbaztec/coveralls-jacoco-gradle-plugin

See: https://github.com/kt3k/coveralls-gradle-plugin/issues/119

14751 of 22639 branches covered (65.16%)

Branch coverage included in aggregate %.

31091 of 37767 relevant lines covered (82.32%)

0.82 hits per line

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

78.15
/plugin/common/src/main/java/com/perl5/lang/perl/parser/elementTypes/PerlReparseableTokenType.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.parser.elementTypes;
18

19
import com.intellij.lang.ASTFactory;
20
import com.intellij.lang.ASTNode;
21
import com.intellij.lang.Language;
22
import com.intellij.lexer.Lexer;
23
import com.intellij.openapi.diagnostic.Logger;
24
import com.intellij.openapi.util.TextRange;
25
import com.intellij.openapi.util.text.StringUtil;
26
import com.intellij.psi.impl.source.tree.FileElement;
27
import com.intellij.psi.impl.source.tree.TreeElement;
28
import com.intellij.psi.impl.source.tree.TreeUtil;
29
import com.intellij.psi.tree.IElementType;
30
import com.intellij.psi.tree.IReparseableLeafElementType;
31
import com.perl5.lang.perl.lexer.PerlLexingContext;
32
import com.perl5.lang.perl.lexer.PerlTokenSets;
33
import com.perl5.lang.perl.lexer.adapters.PerlMergingLexerAdapter;
34
import org.jetbrains.annotations.NotNull;
35
import org.jetbrains.annotations.Nullable;
36

37
public abstract class PerlReparseableTokenType extends PerlTokenType implements IReparseableLeafElementType<ASTNode> {
38
  private static final Logger LOG = Logger.getInstance(PerlReparseableTokenType.class);
1✔
39

40
  public PerlReparseableTokenType(@NotNull String debugName) {
41
    super(debugName);
1✔
42
  }
1✔
43

44
  public PerlReparseableTokenType(@NotNull String debugName, @Nullable Language language) {
45
    super(debugName, language);
1✔
46
  }
1✔
47

48
  /**
49
   * @return true iff {@code newText} may replace the {@code leaf} text without breaking things
50
   */
51
  protected boolean isReparseable(@NotNull ASTNode leaf, @NotNull CharSequence newText) {
52
    TextRange confirmationRange = getLexerConfirmationRange(leaf);
1✔
53
    if (confirmationRange.isEmpty()) {
1✔
54
      LOG.debug("No confirmation range found for ", leaf);
1✔
55
      return false;
1✔
56
    }
57

58
    TextRange leafTextRange = leaf.getTextRange();
1✔
59
    if (!confirmationRange.contains(leafTextRange)) {
1!
60
      LOG.error("Confirmation range must cover the leaf: " +
×
61
                "; confirmation range: " + confirmationRange +
62
                "; leaf: " + leaf +
63
                "; leafRange: " + leafTextRange);
64
      return false;
×
65
    }
66

67
    var quoteLikeParent = TreeUtil.findParent(leaf, PerlTokenSets.ELEMENTS_WITH_CUSTOM_DELIMITERS);
1✔
68
    while (quoteLikeParent != null) {
1✔
69
      var nextQuoteLikeParent = TreeUtil.findParent(quoteLikeParent, PerlTokenSets.ELEMENTS_WITH_CUSTOM_DELIMITERS);
1✔
70
      if (nextQuoteLikeParent == null) {
1!
71
        break;
1✔
72
      }
73
      quoteLikeParent = nextQuoteLikeParent;
×
74
    }
×
75

76
    if (quoteLikeParent != null) {
1✔
77
      confirmationRange = confirmationRange.union(quoteLikeParent.getTextRange());
1✔
78
    }
79

80
    Lexer lexer = createLexer(leaf);
1✔
81
    if (lexer == null) {
1!
82
      LOG.debug("Unable to create lexer for ", leaf);
×
83
      return false;
×
84
    }
85

86
    FileElement fileElement = TreeUtil.getFileElement((TreeElement)leaf);
1✔
87
    if (fileElement == null) {
1!
88
      LOG.debug("No file found for ", leaf);
×
89
      return false;
×
90
    }
91
    CharSequence currentFileChars = fileElement.getChars();
1✔
92
    String textToRelex = currentFileChars.subSequence(confirmationRange.getStartOffset(), leaf.getStartOffset()).toString() +
1✔
93
                         newText +
94
                         currentFileChars.subSequence(leaf.getStartOffset() + leaf.getTextLength(), confirmationRange.getEndOffset());
1✔
95
    if (LOG.isDebugEnabled()) {
1!
96
      LOG.debug("Re-lexing for ", leaf,
1✔
97
                "; range: ", confirmationRange,
98
                "; lenght: ", confirmationRange.getLength(),
1✔
99
                "; text: ", StringUtil.shortenTextWithEllipsis(textToRelex, 40, 20));
1✔
100
    }
101
    lexer.start(textToRelex);
1✔
102
    int newTokenStartOffset = leaf.getStartOffset() - confirmationRange.getStartOffset();
1✔
103
    int newTokenEndOffset = newTokenStartOffset + newText.length();
1✔
104
    while (lexer.getTokenType() != null) {
1!
105
      int startOffset = lexer.getTokenStart();
1✔
106
      if (startOffset == newTokenStartOffset) {
1✔
107
        if (lexer.getTokenEnd() == newTokenEndOffset && lexer.getTokenType() == this) {
1✔
108
          lexer.advance();
1✔
109
          break;
1✔
110
        }
111
        LOG.debug("Token end or end mismatch for  ", leaf, " with text: ", newText);
1✔
112
        return false;
1✔
113
      }
114
      if (lexer.getTokenEnd() > newTokenStartOffset) {
1✔
115
        LOG.debug("Tokens structure different for ", leaf, " with text: ", newText);
1✔
116
        return false;
1✔
117
      }
118
      lexer.advance();
1✔
119
    }
1✔
120

121
    // checking tokens after leaf
122
    int offsetShift = leafTextRange.getEndOffset() - newTokenEndOffset;
1✔
123
    ASTNode run = TreeUtil.nextLeaf(leaf);
1✔
124
    while (true) {
125
      IElementType tokenType = lexer.getTokenType();
1✔
126
      if (tokenType == null) {
1✔
127
        break;
1✔
128
      }
129
      if (run == null) {
1!
130
        return false;
×
131
      }
132
      if (tokenType != run.getElementType()) {
1✔
133
        if (LOG.isDebugEnabled()) {
1!
134
          LOG.debug("Different token types afterwards for ", leaf,
1✔
135
                    "; expected: ", run.getElementType(),
1✔
136
                    "; got: ", tokenType,
137
                    "; token text: ", newText);
138
        }
139
        return false;
1✔
140
      }
141
      if (lexer.getTokenEnd() != run.getTextRange().getEndOffset() - offsetShift) {
1!
142
        if (LOG.isDebugEnabled()) {
×
143
          LOG.debug("Different token ranges afterwards for ", leaf,
×
144
                    "; expected: ", run.getTextRange().getEndOffset(),
×
145
                    "; got: ", lexer.getTokenEnd() + offsetShift,
×
146
                    "; type: ", tokenType,
147
                    "; text: ", lexer.getTokenText(),
×
148
                    "; token text: ", newText);
149
        }
150
        return false;
×
151
      }
152

153
      lexer.advance();
1✔
154
      run = TreeUtil.nextLeaf(run);
1✔
155
    }
1✔
156
    LOG.debug("Leaf ", leaf, " is reparseable with content: ", newText);
1✔
157
    return true;
1✔
158
  }
159

160
  protected @Nullable Lexer createLexer(@NotNull ASTNode nodeToLex) {
161
    return new PerlMergingLexerAdapter(PerlLexingContext.create(nodeToLex.getPsi().getProject()).withEnforcedSublexing(true));
1✔
162
  }
163

164
  /**
165
   * @return range of the file text which is safe to re-lex to check this token for consistency or empty range if not possible
166
   * @apiNote range should start from 0 lexer state
167
   */
168
  protected abstract @NotNull TextRange getLexerConfirmationRange(@NotNull ASTNode leaf);
169

170
  @Override
171
  public final @Nullable ASTNode reparseLeaf(@NotNull ASTNode leaf, @NotNull CharSequence newText) {
172
    return isReparseable(leaf, newText) ? ASTFactory.leaf(this, newText) : null;
1✔
173
  }
174
}
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