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

Camelcade / Perl5-IDEA / #525521703

09 Nov 2025 10:57AM UTC coverage: 75.97% (+0.06%) from 75.91%
#525521703

push

github

hurricup
Safer work with variable name

14772 of 22647 branches covered (65.23%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

89 existing lines in 10 files now uncovered.

31098 of 37732 relevant lines covered (82.42%)

0.82 hits per line

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

79.56
/plugin/common/src/main/java/com/perl5/lang/perl/psi/PerlAssignExpression.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;
18

19
import com.intellij.openapi.util.TextRange;
20
import com.intellij.openapi.util.text.StringUtil;
21
import com.intellij.psi.PsiElement;
22
import com.intellij.psi.util.PsiTreeUtil;
23
import com.intellij.util.SmartList;
24
import com.intellij.util.containers.ContainerUtil;
25
import com.perl5.lang.perl.psi.utils.PerlContextType;
26
import com.perl5.lang.perl.psi.utils.PerlPsiUtil;
27
import com.perl5.lang.perl.util.PerlArrayUtilCore;
28
import com.perl5.lang.perl.util.PerlContextUtil;
29
import org.jetbrains.annotations.Contract;
30
import org.jetbrains.annotations.NotNull;
31
import org.jetbrains.annotations.Nullable;
32

33
import java.util.ArrayList;
34
import java.util.Collections;
35
import java.util.List;
36
import java.util.Objects;
37

38
import static com.perl5.lang.perl.psi.utils.PerlContextType.LIST;
39
import static com.perl5.lang.perl.psi.utils.PerlContextType.SCALAR;
40

41

42
public interface PerlAssignExpression extends PsiPerlExpr {
43
  /**
44
   * Returns the leftmost side of assign expression
45
   *
46
   * @return left side
47
   */
48
  default @NotNull PsiElement getLeftSide() {
49
    return getFirstChild();
1!
50
  }
51

52
  /**
53
   * Returns the rightmost side of assignment expression
54
   *
55
   * @return rightmost side or null if expression is incomplete
56
   */
57
  default @Nullable PsiElement getRightSide() {
58
    PsiElement lastChild = getLastChild();
1✔
59
    PsiElement firstChild = getFirstChild();
1✔
60

61
    if (lastChild == firstChild || lastChild == null || lastChild.getFirstChild() == null) {
1!
62
      return null;
×
63
    }
64

65
    return lastChild;
1✔
66
  }
67

68
  /**
69
   * @return the operator element on the right from assignment element
70
   */
71
  @Contract("null->null")
72
  default @Nullable PsiElement getRightOperatorElement(@Nullable PsiElement assignmentElement) {
73
    var assignmentElementRange = assignmentElement == null ? null : assignmentElement.getTextRange();
1!
74
    if (assignmentElementRange == null || !getTextRange().contains(assignmentElementRange)) {
1!
75
      return null;
×
76
    }
77
    PsiElement anchor = null;
1✔
78
    for (PsiElement child : getChildren()) {
1!
79
      if (child.getTextRange().contains(assignmentElementRange)) {
1✔
80
        anchor = child;
1✔
81
        break;
1✔
82
      }
83
    }
84
    if (anchor == null) {
1!
85
      return null;
×
86
    }
87
    PsiElement result = anchor.getPrevSibling();
1✔
88
    while (result != null) {
1!
89
      if (!PerlPsiUtil.isCommentOrSpace(result)) {
1✔
90
        return result;
1✔
91
      }
92
      result = result.getPrevSibling();
1✔
93
    }
94
    return null;
×
95
  }
96

97
  /**
98
   * @return a left counterpart of the assignment for the right side.
99
   * @implNote current implementation works properly only for simple a = b expressions.
100
   */
101
  default @Nullable PsiElement getLeftPartOfAssignment(@NotNull PsiElement value) {
102
    PsiElement rightSide = getRightSide();
1✔
103
    if (rightSide == null) {
1!
104
      return null;
×
105
    }
106
    List<PsiElement> elements = flattenAssignmentPart(getRightSide());
1✔
107
    if (elements.size() != 1 || !elements.getFirst().equals(value)) {
1!
108
      return null;
×
109
    }
110
    return getLeftSide();
1✔
111
  }
112

113
  /**
114
   * @return a descriptor of value assigned to the {@code leftPartElement} if any. Descriptor contains a list of {@code psiElement}s and index in
115
   * the element, if it's necessary. null means there is no element, or assignment, or it's the rightmost part of it.
116
   * <br/>
117
   * In case of {@code my ($var, $var2) = @_;} for {@code $var2} will return a descriptor with
118
   * {@code @_} as PsiElement and 1 as index.
119
   * <br/>
120
   * In case of transitive assignments, always returns rightmost value. E.g. {@code $var1 = $var2 = $var3;} will return a
121
   * {@code $var3} for {@code $var1} and {@code $var2}
122
   * <br/>
123
   * In case of {@code my @arr = ($var1, $var2)} will return a descriptor of {@code $var1} and {@code $var2} and zero index.
124
   * fixme use real right part. To handle my $var = undef = $var;
125
   */
126
  default @Nullable PerlAssignValueDescriptor getRightPartOfAssignment(@NotNull PsiElement leftPartElement) {
127
    List<PsiElement> children = PerlPsiUtil.cleanupChildren(getChildren());
1✔
128
    if (children.size() < 2) {
1!
129
      return null;
×
130
    }
131
    TextRange leftPartTextRange = leftPartElement.getTextRange();
1✔
132
    PsiElement leftAssignPart = null;
1✔
133
    PsiElement rightAssignPart = null;
1✔
134
    for (int i = 0; i < children.size() - 1; i++) {
1!
135
      PsiElement child = children.get(i);
1✔
136
      if (child.getTextRange().contains(leftPartTextRange)) {
1✔
137
        leftAssignPart = child;
1✔
138
        rightAssignPart = children.get(i + 1);
1✔
139
        break;
1✔
140
      }
141
    }
142
    if (leftAssignPart == null) {
1!
143
      return null;
×
144
    }
145

146
    boolean found = false;
1✔
147
    int leftElementIndex = 0;
1✔
148
    for (PsiElement leftElement : flattenAssignmentPart(leftAssignPart)) {
1!
149
      if (leftElement.getTextRange().equals(leftPartTextRange)) {
1✔
150
        found = true;
1✔
151
        break;
1✔
152
      }
153
      if (PerlContextUtil.isList(leftElement)) {
1✔
154
        return null;
1✔
155
      }
156
      leftElementIndex++;
1✔
157
    }
1✔
158

159
    if (!found) {
1!
160
      return null;
×
161
    }
162

163
    List<PsiElement> rightElements = flattenAssignmentPart(rightAssignPart);
1✔
164
    if (rightElements.isEmpty()) {
1✔
165
      return null;
1✔
166
    }
167
    if (PerlContextUtil.isScalar(leftAssignPart)) {
1✔
168
      PsiElement lastItem = ContainerUtil.getLastItem(rightElements);
1✔
169
      return new PerlAssignValueDescriptor(Objects.requireNonNull(lastItem), PerlContextUtil.isList(lastItem) ? -1 : 0);
1✔
170
    }
171

172
    PerlContextType leftContextType = PerlContextUtil.contextFrom(leftPartElement);
1✔
173
    for (int i = 0; i < rightElements.size(); i++) {
1✔
174
      PsiElement rightElement = rightElements.get(i);
1✔
175
      if (leftElementIndex == 0) {
1✔
176
        if (leftContextType == SCALAR) {
1✔
177
          return new PerlAssignValueDescriptor(rightElement);
1✔
178
        }
179
        return new PerlAssignValueDescriptor(rightElements.subList(i, rightElements.size()));
1✔
180
      }
181
      if (PerlContextUtil.contextFrom(rightElement) == LIST) {
1✔
182
        return new PerlAssignValueDescriptor(rightElements.subList(i, rightElements.size()), leftElementIndex);
1✔
183
      }
184
      leftElementIndex--;
1✔
185
    }
186
    return null;
1✔
187
  }
188

189
  /**
190
   * @return an assignment expression if {@code element} is a part of one.
191
   * Unwraps multi-variable declarations and passing through any empty wrappers, e.g. variable declaration
192
   */
193
  @Contract("null->null")
194
  static @Nullable PerlAssignExpression getAssignmentExpression(@Nullable PsiElement element) {
195
    if (element == null) {
1!
196
      return null;
×
197
    }
198
    PerlAssignExpression assignExpression = PsiTreeUtil.getParentOfType(element, PerlAssignExpression.class);
1✔
199
    if (assignExpression == null) {
1✔
200
      return null;
1✔
201
    }
202

203
    TextRange elementTextRange = element.getTextRange();
1✔
204
    PsiElement container = ContainerUtil.find(assignExpression.getChildren(), it -> it.getTextRange().contains(elementTextRange));
1✔
205

206
    if (element.equals(container)) {
1✔
207
      return assignExpression;
1✔
208
    }
209

210
    return (container != null &&
1!
211
            ContainerUtil.find(flattenAssignmentPart(container), it -> it != null && it.getTextRange().equals(elementTextRange)) != null) ?
1!
212
           assignExpression : null;
1✔
213
  }
214

215
  /**
216
   * Flattens {@code element} by collecting list of elements and unpacking list of declarations.
217
   *
218
   * @return Flattered list of assignment participants
219
   */
220
  static @NotNull List<PsiElement> flattenAssignmentPart(@NotNull PsiElement element) {
221
    List<PsiElement> result = new SmartList<>();
1✔
222
    for (PsiElement listElement : PerlArrayUtilCore.collectListElements(element)) {
1✔
223
      if (listElement instanceof PerlVariableDeclarationExpr) {
1✔
224
        ContainerUtil.addAll(result, listElement.getChildren());
1✔
225
      }
226
      else {
227
        result.add(listElement);
1✔
228
      }
229
    }
1✔
230
    return ContainerUtil.map(
1!
231
      result, it -> it instanceof PerlVariableDeclarationElement && it.getFirstChild() != null ? it.getFirstChild() : it);
1!
232
  }
233

234
  class PerlAssignValueDescriptor {
235
    public static final PerlAssignValueDescriptor EMPTY = new PerlAssignValueDescriptor(Collections.emptyList(), 0);
1✔
236

237
    private final @NotNull List<PsiElement> myElements;
238
    private final int myStartIndex;
239

240
    public PerlAssignValueDescriptor(@NotNull PsiElement element) {
241
      this(Collections.singletonList(element), 0);
1✔
242
    }
1✔
243

244
    public PerlAssignValueDescriptor(@NotNull List<? extends PsiElement> elements) {
245
      this(elements, 0);
1✔
246
    }
1✔
247

248
    public PerlAssignValueDescriptor(@NotNull PsiElement element, int startIndex) {
249
      this(Collections.singletonList(element), startIndex);
1✔
250
    }
1✔
251

252
    public PerlAssignValueDescriptor(@NotNull List<? extends PsiElement> elements, int startIndex) {
1✔
253
      myElements = unflattenElements(elements);
1✔
254
      myStartIndex = startIndex;
1✔
255
    }
1✔
256

257
    private List<PsiElement> unflattenElements(@NotNull List<? extends PsiElement> sourceElements) {
258
      if (sourceElements.isEmpty()) {
1✔
259
        return Collections.emptyList();
1✔
260
      }
261
      List<PsiElement> result;
262
      while (true) {
263
        result = new ArrayList<>();
1✔
264
        boolean modified = false;
1✔
265
        for (int i = 0; i < sourceElements.size(); i++) {
1✔
266
          PsiElement element = sourceElements.get(i);
1✔
267
          PsiElement parent = element.getParent();
1✔
268
          PsiElement[] children = parent.getChildren();
1✔
269
          if (children.length == 1) {
1✔
270
            result.add(parent);
1✔
271
            modified = true;
1✔
272
            continue;
1✔
273
          }
274

275
          if (element.equals(children[0])) {
1✔
276
            int lastElementIndex = sourceElements.indexOf(children[children.length - 1]);
1✔
277
            if (lastElementIndex > 0) {
1✔
278
              result.add(parent);
1✔
279
              modified = true;
1✔
280
              i = lastElementIndex;
1✔
281
              continue;
1✔
282
            }
283
          }
284

285
          result.add(element);
1✔
286
        }
287
        if (!modified) {
1✔
288
          break;
1✔
289
        }
290
        sourceElements = result;
1✔
291
      }
1✔
292
      // falling through parens
293
      result = ContainerUtil.map(result, it -> {
1✔
294
        while (it instanceof PsiPerlParenthesisedExpr) {
1✔
295
          PsiElement[] children = it.getChildren();
1✔
296
          if (children.length == 1) {
1!
297
            it = children[0];
1✔
298
          }
299
          else {
300
            break;
301
          }
302
        }
1✔
303
        return it;
1✔
304
      });
305

306
      return result;
1✔
307
    }
308

309
    public @NotNull List<PsiElement> getElements() {
310
      return myElements;
1!
311
    }
312

313
    /**
314
     * @return start index in the target. E.g. for second element of {@code @list} it's going to be 1. For last one {@code -1} just like in
315
     * perl itself
316
     */
317
    public int getStartIndex() {
318
      return myStartIndex;
1✔
319
    }
320

321
    @Override
322
    public String toString() {
323
      if (myElements.isEmpty()) {
1✔
324
        return "Empty assign expression";
1✔
325
      }
326
      return StringUtil.join(ContainerUtil.map(myElements, PsiElement::toString), ", ") +
1✔
327
             (myStartIndex == 0 ? "" : " [" + myStartIndex + "]");
1✔
328
    }
329

330
    public @Nullable String getText() {
331
      if (myElements.isEmpty()) {
1!
UNCOV
332
        return null;
×
333
      }
334
      PsiElement firstElement = myElements.getFirst();
1✔
335
      PsiElement lastElement = Objects.requireNonNull(ContainerUtil.getLastItem(myElements));
1✔
336
      return firstElement.getContainingFile().getText().substring(
1✔
337
        firstElement.getTextRange().getStartOffset(), lastElement.getTextRange().getEndOffset());
1✔
338
    }
339

340
    @Override
341
    public boolean equals(Object o) {
342
      if (this == o) {
×
UNCOV
343
        return true;
×
344
      }
345
      if (o == null || getClass() != o.getClass()) {
×
UNCOV
346
        return false;
×
347
      }
348

UNCOV
349
      PerlAssignValueDescriptor that = (PerlAssignValueDescriptor)o;
×
350

351
      if (myStartIndex != that.myStartIndex) {
×
UNCOV
352
        return false;
×
353
      }
UNCOV
354
      return myElements.equals(that.myElements);
×
355
    }
356

357
    @Override
358
    public int hashCode() {
359
      int result = myElements.hashCode();
×
360
      result = 31 * result + myStartIndex;
×
UNCOV
361
      return result;
×
362
    }
363
  }
364
}
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