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

Camelcade / Perl5-IDEA / #525521557

29 May 2025 01:55PM UTC coverage: 82.275% (+0.001%) from 82.274%
#525521557

push

github

hurricup
Migrated to ProjectActivity

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

104 existing lines in 12 files now uncovered.

30882 of 37535 relevant lines covered (82.28%)

0.82 hits per line

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

96.93
/plugin/core/src/main/java/com/perl5/lang/perl/idea/formatter/PurePerlFormattingContext.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.idea.formatter;
18

19
import com.intellij.formatting.*;
20
import com.intellij.lang.ASTNode;
21
import com.intellij.openapi.diagnostic.Logger;
22
import com.intellij.openapi.editor.Document;
23
import com.intellij.openapi.util.TextRange;
24
import com.intellij.openapi.util.text.StringUtil;
25
import com.intellij.psi.PsiElement;
26
import com.intellij.psi.TokenType;
27
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
28
import com.intellij.psi.formatter.FormatterUtil;
29
import com.intellij.psi.impl.source.tree.CompositeElement;
30
import com.intellij.psi.impl.source.tree.LeafElement;
31
import com.intellij.psi.impl.source.tree.TreeUtil;
32
import com.intellij.psi.tree.IElementType;
33
import com.intellij.psi.util.PsiTreeUtil;
34
import com.intellij.psi.util.PsiUtilCore;
35
import com.intellij.util.containers.ContainerUtil;
36
import com.intellij.util.containers.FactoryMap;
37
import com.intellij.util.containers.MultiMap;
38
import com.perl5.lang.perl.PerlLanguage;
39
import com.perl5.lang.perl.idea.formatter.blocks.PerlAstBlock;
40
import com.perl5.lang.perl.idea.formatter.blocks.PerlSyntheticBlock;
41
import com.perl5.lang.perl.idea.formatter.settings.PerlCodeStyleSettings;
42
import com.perl5.lang.perl.lexer.PerlTokenSets;
43
import com.perl5.lang.perl.psi.PerlInterpolationContainer;
44
import com.perl5.lang.perl.psi.PerlSignatureElement;
45
import com.perl5.lang.perl.psi.PsiPerlStatementModifier;
46
import com.perl5.lang.perl.psi.utils.PerlPsiUtil;
47
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
48
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
49
import org.jetbrains.annotations.Contract;
50
import org.jetbrains.annotations.NotNull;
51
import org.jetbrains.annotations.Nullable;
52

53
import java.util.HashMap;
54
import java.util.List;
55
import java.util.Map;
56

57
import static com.intellij.formatting.WrapType.*;
58
import static com.intellij.psi.codeStyle.CommonCodeStyleSettings.*;
59
import static com.perl5.lang.perl.idea.formatter.PerlFormattingTokenSets.*;
60
import static com.perl5.lang.perl.idea.formatter.settings.PerlCodeStyleSettings.OptionalConstructions.*;
61
import static com.perl5.lang.perl.lexer.PerlTokenSets.*;
62
import static com.perl5.lang.perl.parser.PerlElementTypesGenerated.*;
63

64
public class PurePerlFormattingContext extends PerlBaseFormattingContext {
65
  private static final Logger LOG = Logger.getInstance(PurePerlFormattingContext.class);
1✔
66
  private final Map<ASTNode, Wrap> myWrapMap = new HashMap<>();
1✔
67
  private final Map<Integer, Alignment> myAssignmentsAlignmentsMap = new HashMap<>();
1✔
68
  private final Int2ObjectOpenHashMap<Alignment> myCommentsAlignmentMap = new Int2ObjectOpenHashMap<>();
1✔
69
  private final Int2ObjectOpenHashMap<Alignment> myAnnotationTypesAlignmentMap = new Int2ObjectOpenHashMap<>();
1✔
70
  private final Map<ASTNode, Alignment> myRightwardCallsAlignmentMap = FactoryMap.create(sequence -> Alignment.createAlignment(true));
1✔
71
  private final Map<ASTNode, Alignment> myOperatorsAlignmentsMap = FactoryMap.create(sequence -> Alignment.createAlignment(true));
1✔
72
  private final Map<ASTNode, Alignment> myElementsALignmentsMap = FactoryMap.create(sequence -> Alignment.createAlignment(true));
1✔
73
  private final Map<ASTNode, Alignment> myAssignmentsElementAlignmentsMap = FactoryMap.create(sequence -> Alignment.createAlignment(true));
1✔
74
  private final Map<ASTNode, Map<ASTNode, Alignment>> myStringListAlignmentMap = FactoryMap.create(listNode -> {
1✔
75
    Map<Integer, Alignment> generatingMap = FactoryMap.create(key -> Alignment.createAlignment(true));
1✔
76

77
    int column = 0;
1✔
78
    Map<ASTNode, Alignment> itemsMap = new HashMap<>();
1✔
79
    ASTNode run = listNode.getFirstChildNode();
1✔
80
    while (run != null) {
1✔
81
      if (PsiUtilCore.getElementType(run) == STRING_BARE) {
1✔
82
        itemsMap.put(run, generatingMap.get(column++));
1✔
83
      }
84
      else if (StringUtil.containsLineBreak(run.getChars())) {
1✔
85
        column = 0;
1✔
86
      }
87
      run = run.getTreeNext();
1✔
88
    }
89

90
    return itemsMap;
1✔
91
  });
92

93
  /**
94
   * @return alignment for the line comment node when comments on sequential lines should be aligned
95
   */
96
  private @Nullable Alignment getLineCommentAlignment(@NotNull ASTNode commentNode) {
97
    ASTNode prevNode = commentNode.getTreePrev();
1✔
98
    if (prevNode == null || StringUtil.containsLineBreak(prevNode.getChars())) {
1✔
99
      // no alignment for the only comment on the line
100
      return null;
1✔
101
    }
102

103
    int commentLine = getNodeLine(commentNode);
1✔
104
    if (myCommentsAlignmentMap.containsKey(commentLine)) {
1✔
105
      return myCommentsAlignmentMap.get(commentLine);
1✔
106
    }
107
    if (LOG.isDebugEnabled()) {
1✔
108
      LOG.debug("Checking comment line ", commentLine, " - ", commentNode, " - ", commentNode.getChars());
1✔
109
    }
110

111
    ASTNode prevLineComment = getPreviousLineElement(prevNode, COMMENT_LINE, true);
1✔
112
    logDelegation(commentNode, prevLineComment);
1✔
113
    Alignment alignment = prevLineComment == null ? null : getLineCommentAlignment(prevLineComment);
1✔
114
    if (alignment == null) {
1✔
115
      alignment = Alignment.createAlignment(true);
1✔
116
    }
117
    myCommentsAlignmentMap.put(commentLine, alignment);
1✔
118
    return alignment;
1✔
119
  }
120

121
  /**
122
   * @return alignment for the line comment node when comments on sequential lines should be aligned
123
   */
124
  private @Nullable Alignment getAnnotationTypeAlignment(@NotNull ASTNode commentAnnotationNode) {
125
    if (commentAnnotationNode.getElementType() != COMMENT_ANNOTATION) {
1✔
UNCOV
126
      LOG.error("Expected " + COMMENT_ANNOTATION + ", got " + commentAnnotationNode.getElementType());
×
UNCOV
127
      return null;
×
128
    }
129
    ASTNode prevNode = commentAnnotationNode.getTreePrev();
1✔
130
    int annotationLine = getNodeLine(commentAnnotationNode);
1✔
131
    if (myAnnotationTypesAlignmentMap.containsKey(annotationLine)) {
1✔
132
      return myAnnotationTypesAlignmentMap.get(annotationLine);
1✔
133
    }
134
    if (LOG.isDebugEnabled()) {
1✔
135
      LOG.debug("Checking type specifier at line ", annotationLine, " - ", commentAnnotationNode, " - ", commentAnnotationNode.getChars());
1✔
136
    }
137

138
    ASTNode prevLineAnnotation = getPreviousLineElement(prevNode, COMMENT_ANNOTATION, false);
1✔
139
    logDelegation(commentAnnotationNode, prevLineAnnotation);
1✔
140
    Alignment alignment = prevLineAnnotation == null ? null : getAnnotationTypeAlignment(prevLineAnnotation);
1✔
141
    if (alignment == null) {
1✔
142
      alignment = Alignment.createAlignment(true);
1✔
143
    }
144
    myAnnotationTypesAlignmentMap.put(annotationLine, alignment);
1✔
145
    return alignment;
1✔
146
  }
147

148
  private static void logDelegation(@NotNull ASTNode commentAnnotationNode, @Nullable ASTNode prevLineAnnotation) {
149
    if (prevLineAnnotation != null && LOG.isDebugEnabled()) {
1✔
150
      LOG.debug("Delegating to previous line comment: ", commentAnnotationNode, " - ", commentAnnotationNode.getChars());
1✔
151
    }
152
  }
1✔
153

154
  /**
155
   * @param leafNode if true, we are iterating through leaves, not composite elements
156
   * @return element of {@code elementType} separated from the {@code node} by a spaces with a single newline.
157
   */
158
  private static @Nullable ASTNode getPreviousLineElement(@Nullable ASTNode node, @NotNull IElementType elementType, boolean leafNode) {
159
    while (node != null) {
1✔
160
      CharSequence nodeChars = node.getChars();
1✔
161
      int newLinesNumber = StringUtil.countChars(nodeChars, '\n');
1✔
162
      if (newLinesNumber > 1) {
1✔
163
        return null;
1✔
164
      }
165
      node = leafNode ? TreeUtil.prevLeaf(node) : node.getTreePrev();
1✔
166
      if (newLinesNumber == 1) {
1✔
167
        break;
1✔
168
      }
169
    }
1✔
170

171
    return node != null && node.getElementType() == elementType ? node : null;
1✔
172
  }
173

174
  /**
175
   * Maps line numbers to the offset of first here-doc openers. Or {@link Integer.MAX_VALUE} if there is none
176
   */
177
  private final Int2IntOpenHashMap myHeredocForbiddenOffsets = new Int2IntOpenHashMap();
1✔
178

179
  private static final MultiMap<IElementType, IElementType> OPERATOR_COLLISIONS_MAP = new MultiMap<>();
1✔
180

181
  static {
182
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_PLUS_PLUS, OPERATOR_PLUS);
1✔
183
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_PLUS, OPERATOR_PLUS_PLUS);
1✔
184
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_PLUS, OPERATOR_PLUS);
1✔
185
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_MINUS_MINUS, OPERATOR_MINUS);
1✔
186
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_MINUS, OPERATOR_MINUS_MINUS);
1✔
187
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_MINUS, OPERATOR_MINUS);
1✔
188
    OPERATOR_COLLISIONS_MAP.putValue(OPERATOR_SMARTMATCH, OPERATOR_BITWISE_NOT);
1✔
189
  }
1✔
190

191
  public PurePerlFormattingContext(@NotNull FormattingContext formattingContext) {
192
    super(formattingContext);
1✔
193
  }
1✔
194

195
  public PurePerlFormattingContext(@NotNull FormattingContext formattingContext, @NotNull TextRange adjustedRange) {
196
    this(FormattingContext.create(
1✔
197
      formattingContext.getPsiElement(),
1✔
198
      adjustedRange,
199
      formattingContext.getCodeStyleSettings(),
1✔
200
      formattingContext.getFormattingMode()
1✔
201
    ));
202
  }
1✔
203

204
  @Override
205
  protected @NotNull SpacingBuilder createSpacingBuilder() {
206
    return PerlSpacingBuilderFactory.createSpacingBuilder(getSettings(), getPerlSettings());
1✔
207
  }
208

209
  public @NotNull CommonCodeStyleSettings getSettings() {
210
    return getCodeStyleSettings().getCommonSettings(PerlLanguage.INSTANCE);
1✔
211
  }
212

213
  public @NotNull PerlCodeStyleSettings getPerlSettings() {
214
    return getCodeStyleSettings().getCustomSettings(PerlCodeStyleSettings.class);
1✔
215
  }
216

217
  public @NotNull PerlIndentProcessor getIndentProcessor() {
218
    return PerlIndentProcessor.INSTANCE;
1✔
219
  }
220

221
  @Override
222
  public @NotNull Indent getNodeIndent(@NotNull ASTNode node) {
223
    return getIndentProcessor().getNodeIndent(node);
1✔
224
  }
225

226
  @Override
227
  public @Nullable Indent getChildIndent(@NotNull PerlAstBlock block, int newChildIndex) {
228
    return getIndentProcessor().getChildIndent(block, newChildIndex);
1✔
229
  }
230

231
  /**
232
   * Checks if Heredoc is ahead of current block and it's not possible to insert newline
233
   *
234
   * @param node in question
235
   * @return check result
236
   */
237
  public boolean isNewLineForbiddenAt(@NotNull ASTNode node) {
238
    if (getDocument() == null) {
1✔
UNCOV
239
      return true;
×
240
    }
241
    int nodeLine = getNodeLine(node);
1✔
242
    if (myHeredocForbiddenOffsets.containsKey(nodeLine)) {
1✔
243
      return node.getStartOffset() > myHeredocForbiddenOffsets.get(nodeLine);
1✔
244
    }
245

246
    int heredocOffset = Integer.MAX_VALUE;
1✔
247
    LeafElement firstLeaf = TreeUtil.findFirstLeaf(node);
1✔
248
    ASTNode run = firstLeaf;
1✔
249
    while (run != null) {
1✔
250
      if (run.getElementType() == OPERATOR_HEREDOC) {
1✔
251
        heredocOffset = run.getStartOffset();
1✔
252
      }
253
      else if (StringUtil.containsLineBreak(run.getChars())) {
1✔
254
        break;
1✔
255
      }
256
      run = TreeUtil.prevLeaf(run);
1✔
257
    }
258

259
    if (heredocOffset == Integer.MAX_VALUE) {
1✔
260
      run = firstLeaf;
1✔
261
      while (run != null) {
1✔
262
        if (run.getElementType() == OPERATOR_HEREDOC) {
1✔
263
          heredocOffset = run.getStartOffset();
1✔
264
          break;
1✔
265
        }
266
        else if (StringUtil.containsLineBreak(run.getChars())) {
1✔
267
          break;
1✔
268
        }
269
        run = TreeUtil.nextLeaf(run);
1✔
270
      }
271
    }
272
    myHeredocForbiddenOffsets.put(nodeLine, heredocOffset);
1✔
273
    return node.getStartOffset() > heredocOffset;
1✔
274
  }
275

276

277
  @Override
278
  public @Nullable Spacing getSpacing(@Nullable ASTBlock parent, @Nullable Block child1, @NotNull Block child2) {
279
    if (parent instanceof PerlSyntheticBlock syntheticBlock) {
1✔
280
      parent = syntheticBlock.getRealBlock();
1✔
281
    }
282

283
    if (child1 instanceof PerlSyntheticBlock syntheticBlock) {
1✔
284
      child1 = syntheticBlock.getLastRealBlock();
1✔
285
    }
286

287
    if (child2 instanceof PerlSyntheticBlock syntheticBlock) {
1✔
288
      child2 = syntheticBlock.getFirstRealBlock();
1✔
289
    }
290

291
    if (child1 instanceof ASTBlock astBlock1 && child2 instanceof ASTBlock astBlock2) {
1✔
292
      ASTNode child1Node = astBlock1.getNode();
1✔
293
      IElementType child1Type = PsiUtilCore.getElementType(child1Node);
1✔
294
      ASTNode child2Node = astBlock2.getNode();
1✔
295
      IElementType child2Type = PsiUtilCore.getElementType(child2Node);
1✔
296

297
      ASTNode parentNode = parent != null ? parent.getNode() :
1✔
298
                           child1Node != null ? child1Node.getTreeParent() :
1✔
UNCOV
299
                           child2Node != null ? child2Node.getTreeParent() :
×
300
                           null;
1✔
301
      IElementType parentNodeType = PsiUtilCore.getElementType(parentNode);
1✔
302

303
      if (ALL_QUOTE_OPENERS.contains(child1Type) && child2Node != null) {
1✔
304
        CharSequence openerChars = child2Node.getChars();
1✔
305
        int spaces = 0;
1✔
306
        if (!openerChars.isEmpty() && Character.isUnicodeIdentifierPart(openerChars.charAt(0))) {
1✔
307
          spaces = 1;
1✔
308
        }
309
        return Spacing.createSpacing(spaces, spaces, 0, true, 1);
1✔
310
      }
311

312
      if (parentNodeType == PARENTHESISED_EXPR &&
1✔
313
          (child1Type == LEFT_PAREN || child2Type == RIGHT_PAREN) &&
314
          parentNode.getPsi().getParent() instanceof PsiPerlStatementModifier) {
1✔
315
        return getSettings().SPACE_WITHIN_IF_PARENTHESES ?
1✔
316
               Spacing.createSpacing(1, 1, 0, true, 1) :
1✔
317
               Spacing.createSpacing(0, 0, 0, true, 1);
1✔
318
      }
319

320
      if ((child1Type == OPERATOR_DEREFERENCE || child2Type == OPERATOR_DEREFERENCE) &&
1✔
321
          parentNode.getPsi().getParent() instanceof PerlInterpolationContainer) {
1✔
322
        return Spacing.createSpacing(0, 0, 0, getSettings().KEEP_LINE_BREAKS, getSettings().KEEP_BLANK_LINES_IN_CODE);
1✔
323
      }
324

325
      // fix for number/concat
326
      if (child2Type == OPERATOR_CONCAT) {
1✔
327
        ASTNode run = child1Node;
1✔
328
        while (run instanceof CompositeElement) {
1✔
329
          run = run.getLastChildNode();
1✔
330
        }
331

332
        if (run != null) {
1✔
333
          IElementType runType = run.getElementType();
1✔
334
          if (runType == NUMBER && StringUtil.endsWith(run.getText(), ".")) {
1✔
UNCOV
335
            return Spacing.createSpacing(1, 1, 0, true, 1);
×
336
          }
337
        }
338
      }
339

340
      // LF after opening brace and before closing need to check if here-doc opener is in the line
341
      if (LF_ELEMENTS.contains(child1Type) && LF_ELEMENTS.contains(child2Type) &&
1✔
342
          !(child1Type == LABEL_DECLARATION && child2Type == BLOCK_COMPOUND)) {
343
        if (!isNewLineForbiddenAt(child2Node)) {
1✔
344
          return Spacing.createSpacing(0, 0, 1, true, 1);
1✔
345
        }
346
        else {
347
          return Spacing.createSpacing(1, Integer.MAX_VALUE, 0, true, 1);
1✔
348
        }
349
      }
350

351
      if (BLOCK_LIKE_CONTAINERS.contains(parentNodeType)) {
1✔
352
        boolean afterOpener = BLOCK_OPENERS.contains(child1Type) && isFirst(child1);
1✔
353
        boolean beforeCloser = BLOCK_CLOSERS.contains(child2Type) && isLast(child2);
1✔
354
        IElementType grandParentElementType = PsiUtilCore.getElementType(parentNode.getTreeParent());
1✔
355
        if (PerlTokenSets.CAST_EXPRESSIONS.contains(grandParentElementType)) {
1✔
356
          int spaces = getSettings().SPACE_WITHIN_CAST_PARENTHESES ? 1 : 0;
1✔
357
          return Spacing.createSpacing(spaces, spaces, 0, true, 0);
1✔
358
        }
359

360
        // small inline blocks
361
        if (child1Node != null && grandParentElementType != GREP_EXPR &&
1✔
362
            grandParentElementType != SORT_EXPR && grandParentElementType != MAP_EXPR) {
363
          boolean isSmallBlock = blockHasLessChildrenThan(parentNode, 2);
1✔
364
          boolean isNewLineAllowed = !isNewLineForbiddenAt(child1Node);
1✔
365
          if (!isSmallBlock && (afterOpener || beforeCloser) && isNewLineAllowed) {
1✔
366
            return Spacing.createSpacing(0, 0, 1, true, 1);
1✔
367
          }
368
        }
369
      }
370
      if (parentNodeType == PARENTHESISED_CALL_ARGUMENTS &&
1✔
371
          child2Type == RIGHT_PAREN &&
372
          child1Node != null &&
373
          PsiUtilCore.getElementType(PsiTreeUtil.getDeepestLast(child1Node.getPsi())) == RIGHT_PAREN
1✔
374
      ) {
375
        return Spacing.createSpacing(0, 0, 0, true, 0);
1✔
376
      }
377

378
      // hack for + ++/- --/~~ ~
379
      if ((child2Type == PREFIX_UNARY_EXPR || child2Type == PREF_PP_EXPR) && OPERATOR_COLLISIONS_MAP.containsKey(child1Type)) {
1✔
380
        IElementType rightSignType = PsiUtilCore.getElementType(child2Node.getFirstChildNode());
1✔
381
        if (OPERATOR_COLLISIONS_MAP.get(child1Type).contains(rightSignType)) {
1✔
382
          return Spacing.createSpacing(1, 1, 0, true, 1);
1✔
383
        }
384
      }
1✔
385
      // hack for ++ +/-- -
386
      else if (child1Type == SUFF_PP_EXPR && OPERATOR_COLLISIONS_MAP.containsKey(child2Type)) {
1✔
387
        IElementType leftSignType = PsiUtilCore.getElementType(child1Node.getLastChildNode());
1✔
388
        if (OPERATOR_COLLISIONS_MAP.get(child2Type).contains(leftSignType)) {
1✔
389
          return Spacing.createSpacing(1, 1, 0, true, 1);
1✔
390
        }
391
      }
392

393
      if (child1Type == METHOD && child2Type == CALL_ARGUMENTS &&
1✔
394
          parentNodeType == SUB_CALL && PsiUtilCore.getElementType(parentNode.getTreeParent()) == TYPE_SPECIFIER) {
1✔
395
        return Spacing.createSpacing(0, 0, 0, false, 0);
1✔
396
      }
397
    }
398

399
    return super.getSpacing(parent, child1, child2);
1✔
400
  }
401

402
  /**
403
   * Checks if block contains more than specified number of meaningful children. Spaces and line comments are being ignored
404
   *
405
   * @return check result
406
   */
407
  private static boolean blockHasLessChildrenThan(@NotNull ASTNode node, int maxChildren) {
408
    int counter = -2; // for braces
1✔
409
    ASTNode childNode = node.getFirstChildNode();
1✔
410
    while (childNode != null) {
1✔
411
      IElementType nodeType = childNode.getElementType();
1✔
412
      if (nodeType != TokenType.WHITE_SPACE && nodeType != COMMENT_LINE && nodeType != SEMICOLON) {
1✔
413
        if (++counter >= maxChildren) {
1✔
414
          return false;
1✔
415
        }
416
      }
417
      childNode = childNode.getTreeNext();
1✔
418
    }
1✔
419
    return true;
1✔
420
  }
421

422
  @Override
423
  public @Nullable Alignment getAlignment(@NotNull ASTNode childNode) {
424
    ASTNode parentNode = childNode.getTreeParent();
1✔
425
    IElementType parentNodeType = PsiUtilCore.getElementType(parentNode);
1✔
426
    IElementType childNodeType = PsiUtilCore.getElementType(childNode);
1✔
427
    PerlCodeStyleSettings perlCodeStyleSettings = getPerlSettings();
1✔
428
    var commonCodeStyleSettings = getSettings();
1✔
429

430
    if (perlCodeStyleSettings.ALIGN_RIGHTWARD_CALLS && isRightwardCall(childNode)) {
1✔
431
      var alignment = getRightwardCallAlignment(childNode, false);
1✔
432
      if (alignment != null) {
1✔
433
        return alignment;
1✔
434
      }
435
    }
436

437
    if (childNodeType == FAT_COMMA &&
1✔
438
        parentNodeType == COMMA_SEQUENCE_EXPR &&
439
        perlCodeStyleSettings.ALIGN_FAT_COMMA) {
440
      return myOperatorsAlignmentsMap.get(parentNode);
1✔
441
    }
442
    else if (parentNodeType == TERNARY_EXPR && commonCodeStyleSettings.ALIGN_MULTILINE_TERNARY_OPERATION) {
1✔
443
      return myElementsALignmentsMap.get(parentNode);
1✔
444
    }
445
    else if (parentNodeType == DEREF_EXPR && commonCodeStyleSettings.ALIGN_MULTILINE_CHAINED_METHODS) {
1✔
446
      if (perlCodeStyleSettings.METHOD_CALL_CHAIN_SIGN_NEXT_LINE) {
1✔
447
        if (childNodeType == OPERATOR_DEREFERENCE) {
1✔
448
          return myOperatorsAlignmentsMap.get(parentNode);
1✔
449
        }
450
      }
451
      else {
452
        if (childNodeType != OPERATOR_DEREFERENCE && childNode.getTreePrev() != null) {
1✔
453
          return myOperatorsAlignmentsMap.get(parentNode);
1✔
454
        }
455
      }
456
    }
457
    else if (childNodeType == STRING_BARE && parentNodeType == STRING_LIST && perlCodeStyleSettings.ALIGN_QW_ELEMENTS) {
1✔
458
      return myStringListAlignmentMap.get(parentNode).get(childNode);
1✔
459
    }
460
    else if (childNodeType == COMMENT_LINE && perlCodeStyleSettings.ALIGN_COMMENTS_ON_CONSEQUENT_LINES) {
1✔
461
      return getLineCommentAlignment(childNode);
1✔
462
    }
463
    else if (ANNOTATION_TYPE_SPECIFIERS.contains(childNodeType) && parentNodeType == ANNOTATION_TYPE &&
1✔
464
             perlCodeStyleSettings.ALIGN_ANNOTATION_TYPE_SPECIFIERS) {
465
      return getAnnotationTypeAlignment(parentNode.getTreeParent());
1✔
466
    }
467
    else if (parentNodeType == COMMA_SEQUENCE_EXPR &&
1✔
468
             childNodeType != COMMA &&
469
             childNodeType != FAT_COMMA) {
470
      IElementType grandParentNodeType = PsiUtilCore.getElementType(parentNode.getTreeParent());
1✔
471
      if (grandParentNodeType == CALL_ARGUMENTS || grandParentNodeType == PARENTHESISED_CALL_ARGUMENTS) {
1✔
472
        if (commonCodeStyleSettings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS) {
1✔
473
          return myElementsALignmentsMap.get(parentNode);
1✔
474
        }
475
      }
476
      else if (commonCodeStyleSettings.ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION) {
1✔
477
        return myElementsALignmentsMap.get(parentNode);
1✔
478
      }
479
    }
1✔
480
    else if (parentNodeType == SIGNATURE_CONTENT) {
1✔
481
      return commonCodeStyleSettings.ALIGN_MULTILINE_PARAMETERS ? myElementsALignmentsMap.get(parentNode) : null;
1✔
482
    }
483
    else if ((childNodeType == VARIABLE_DECLARATION_ELEMENT ||
1✔
484
              (childNodeType == UNDEF_EXPR && PerlTokenSets.VARIABLE_DECLARATIONS.contains(parentNodeType))) &&
1✔
485
             perlCodeStyleSettings.ALIGN_VARIABLE_DECLARATIONS) {
486
      return myElementsALignmentsMap.get(parentNode);
1✔
487
    }
488
    else if (BINARY_EXPRESSIONS.contains(parentNodeType) && commonCodeStyleSettings.ALIGN_MULTILINE_BINARY_OPERATION) {
1✔
489
      return myElementsALignmentsMap.get(parentNode);
1✔
490
    }
491
    else if ((parentNodeType == ASSIGN_EXPR || parentNodeType == SIGNATURE_ELEMENT) && OPERATORS_ASSIGNMENT.contains(childNodeType)) {
1✔
492
      if (perlCodeStyleSettings.ALIGN_CONSECUTIVE_ASSIGNMENTS == ALIGN_LINES) {
1✔
493
        return getLineBasedAlignment(childNode, myAssignmentsAlignmentsMap);
1✔
494
      }
495
      else if (perlCodeStyleSettings.ALIGN_CONSECUTIVE_ASSIGNMENTS == ALIGN_IN_STATEMENT) {
1✔
496
        return myAssignmentsElementAlignmentsMap.get(parentNodeType == SIGNATURE_ELEMENT ? parentNode.getTreeParent() : parentNode);
1✔
497
      }
498
    }
499
    else if (perlCodeStyleSettings.ALIGN_ATTRIBUTES && parentNodeType == ATTRIBUTES &&
1✔
500
             (childNodeType == COLON || isAttributeWithoutColon(childNode))) {
1✔
501
      return myElementsALignmentsMap.get(parentNode);
1✔
502
    }
503
    return null;
1✔
504
  }
505

506
  private static boolean isRightwardCall(@NotNull ASTNode node) {
507
    var elementType = node.getElementType();
1✔
508
    if (elementType == SUB_CALL && PsiUtilCore.getElementType(node.getLastChildNode()) != PARENTHESISED_CALL_ARGUMENTS) {
1✔
509
      return true;
1✔
510
    }
511
    return RIGHTWARD_CALL_EXPRESSIONS.contains(elementType);
1✔
512
  }
513

514
  private @Nullable ASTNode getWrappingCallSkippingArgs(@NotNull ASTNode node) {
515
    var parentNode = node.getTreeParent();
1✔
516
    if (parentNode == null) {
1✔
UNCOV
517
      return null;
×
518
    }
519
    var parentNodeType = PsiUtilCore.getElementType(parentNode);
1✔
520
    if (parentNodeType == COMMA_SEQUENCE_EXPR || parentNodeType == CALL_ARGUMENTS) {
1✔
521
      return getWrappingCallSkippingArgs(parentNode);
1✔
522
    }
523
    return isRightwardCall(parentNode) ? parentNode : null;
1✔
524
  }
525

526
  @Contract("null, _ -> null")
527
  private @Nullable Alignment getRightwardCallAlignment(@Nullable ASTNode node, boolean skipNonAlignable) {
528
    if (node == null) {
1✔
529
      return null;
1✔
530
    }
531

532
    boolean shouldBeAligned = isShouldBeAligned(node);
1✔
533
    if (!skipNonAlignable && !shouldBeAligned) {
1✔
534
      return null;
1✔
535
    }
536

537
    var wrappingCall = getWrappingCallSkippingArgs(node);
1✔
538
    if (shouldBeAligned) {
1✔
539
      var wrappingCallAlignment = getRightwardCallAlignment(wrappingCall, true);
1✔
540
      return wrappingCallAlignment != null ? wrappingCallAlignment : myRightwardCallsAlignmentMap.get(node);
1✔
541
    }
542
    return getRightwardCallAlignment(wrappingCall, true);
1✔
543
  }
544

545
  private static boolean isShouldBeAligned(@NotNull ASTNode node) {
546
    var prevNode = node.getTreePrev();
1✔
547
    if (prevNode == null) {
1✔
548
      ASTNode anchor = node;
1✔
549
      if (PsiUtilCore.getElementType(anchor.getTreeParent()) == COMMA_SEQUENCE_EXPR) {
1✔
UNCOV
550
        anchor = anchor.getTreeParent();
×
551
      }
552
      if (PsiUtilCore.getElementType(anchor.getTreeParent()) == CALL_ARGUMENTS) {
1✔
553
        anchor = anchor.getTreeParent();
1✔
554
      }
555
      prevNode = anchor.getTreePrev();
1✔
556
    }
557
    return prevNode != null && StringUtil.containsLineBreak(prevNode.getChars());
1✔
558
  }
559

560
  /**
561
   * Returns line-based alignment for the {@code childNode}. Uses previous line alignment from the {@code alignmentsMap}
562
   * or creates a new one.
563
   *
564
   * @param alignmentsMap map for caching line-based values
565
   */
566
  private @Nullable Alignment getLineBasedAlignment(@NotNull ASTNode childNode, @NotNull Map<Integer, Alignment> alignmentsMap) {
567
    int nodeLine = getNodeLine(childNode);
1✔
568
    if (nodeLine < 0) {
1✔
UNCOV
569
      return null;
×
570
    }
571
    Alignment alignment = alignmentsMap.get(nodeLine);
1✔
572
    if (alignment != null) {
1✔
573
      return alignment;
1✔
574
    }
575
    alignment = alignmentsMap.get(nodeLine - 1);
1✔
576
    if (alignment == null) {
1✔
577
      alignment = alignmentsMap.get(nodeLine + 1);
1✔
578
    }
579
    if (alignment == null) {
1✔
580
      alignment = Alignment.createAlignment(true);
1✔
581
    }
582
    alignmentsMap.put(nodeLine, alignment);
1✔
583
    return alignment;
1✔
584
  }
585

586
  @Override
587
  public @Nullable Wrap getWrap(@NotNull ASTNode childNode) {
588
    ASTNode parentNode = childNode.getTreeParent();
1✔
589
    IElementType parentNodeType = PsiUtilCore.getElementType(parentNode);
1✔
590
    IElementType childNodeType = PsiUtilCore.getElementType(childNode);
1✔
591
    var commonCodeStyleSettings = getSettings();
1✔
592
    var perlCodeStyleSettings = getPerlSettings();
1✔
593

594
    if (isNewLineForbiddenAt(childNode)) {
1✔
595
      return null;
1✔
596
    }
597
    else if (childNodeType == COMMENT_LINE) {
1✔
598
      return commonCodeStyleSettings.WRAP_COMMENTS ? Wrap.createWrap(WRAP_AS_NEEDED, false) : Wrap.createWrap(NONE, false);
1✔
599
    }
600
    else if (parentNodeType == TERNARY_EXPR) {
1✔
601
      if (commonCodeStyleSettings.TERNARY_OPERATION_SIGNS_ON_NEXT_LINE) {
1✔
602
        if (childNodeType == COLON || childNodeType == QUESTION) {
1✔
603
          return getWrapBySettings(parentNode, commonCodeStyleSettings.TERNARY_OPERATION_WRAP, true);
1✔
604
        }
605
      }
606
      else if (childNodeType != COLON && childNodeType != QUESTION) {
1✔
607
        return getWrapBySettings(parentNode, commonCodeStyleSettings.TERNARY_OPERATION_WRAP, false);
1✔
608
      }
609
    }
610
    else if (parentNodeType == SIGNATURE_CONTENT && childNodeType != COMMA && childNodeType != FAT_COMMA) {
1✔
611
      return getWrapBySettings(parentNode, commonCodeStyleSettings.METHOD_PARAMETERS_WRAP, false);
1✔
612
    }
613
    else if (parentNodeType == COMMA_SEQUENCE_EXPR && childNodeType != COMMA && childNodeType != FAT_COMMA) {
1✔
614
      IElementType grandParentNodeType = PsiUtilCore.getElementType(parentNode.getTreeParent());
1✔
615
      if (grandParentNodeType == CALL_ARGUMENTS || grandParentNodeType == PARENTHESISED_CALL_ARGUMENTS) {
1✔
616
        return getWrapBySettings(parentNode, commonCodeStyleSettings.CALL_PARAMETERS_WRAP, false);
1✔
617
      }
618
      else {
619
        return getWrapBySettings(parentNode, commonCodeStyleSettings.ARRAY_INITIALIZER_WRAP, false);
1✔
620
      }
621
    }
622
    else if (parentNodeType == STRING_LIST && (childNodeType == STRING_BARE || childNodeType == QUOTE_SINGLE_CLOSE)) {
1✔
623
      return getWrapBySettings(parentNode, perlCodeStyleSettings.QW_LIST_WRAP, false);
1✔
624
    }
625
    else if (childNodeType == VARIABLE_DECLARATION_ELEMENT && parentNodeType != SIGNATURE_ELEMENT ||
1✔
626
             (childNodeType == UNDEF_EXPR && PerlTokenSets.VARIABLE_DECLARATIONS.contains(parentNodeType))) {
1✔
627
      return getWrapBySettings(parentNode, perlCodeStyleSettings.VARIABLE_DECLARATION_WRAP, false);
1✔
628
    }
629
    else if (parentNodeType == DEREF_EXPR && !QUOTED_STRINGS.contains(PsiUtilCore.getElementType(parentNode.getTreeParent()))) {
1✔
630
      if (perlCodeStyleSettings.METHOD_CALL_CHAIN_SIGN_NEXT_LINE) {
1✔
631
        if (childNodeType == OPERATOR_DEREFERENCE) {
1✔
632
          return getWrapBySettings(parentNode, commonCodeStyleSettings.METHOD_CALL_CHAIN_WRAP, true);
1✔
633
        }
634
      }
635
      else {
636
        if (childNodeType != OPERATOR_DEREFERENCE) {
1✔
637
          return getWrapBySettings(parentNode, commonCodeStyleSettings.METHOD_CALL_CHAIN_WRAP, false);
1✔
638
        }
639
      }
640
    }
641
    else if (BINARY_EXPRESSIONS.contains(parentNodeType)) {
1✔
642
      if (commonCodeStyleSettings.BINARY_OPERATION_SIGN_ON_NEXT_LINE && BINARY_OPERATORS.contains(childNodeType)) {
1✔
643
        return getWrapBySettings(parentNode, commonCodeStyleSettings.BINARY_OPERATION_WRAP, true);
1✔
644
      }
645
      else if (!commonCodeStyleSettings.BINARY_OPERATION_SIGN_ON_NEXT_LINE && !BINARY_OPERATORS.contains(childNodeType)) {
1✔
646
        return getWrapBySettings(parentNode, commonCodeStyleSettings.BINARY_OPERATION_WRAP, false);
1✔
647
      }
648
    }
649
    else if (parentNodeType == ASSIGN_EXPR &&
1✔
650
             OPERATORS_ASSIGNMENT.contains(childNodeType) == commonCodeStyleSettings.PLACE_ASSIGNMENT_SIGN_ON_NEXT_LINE) {
1✔
651
      return getWrapBySettings(parentNode, commonCodeStyleSettings.ASSIGNMENT_WRAP, OPERATORS_ASSIGNMENT.contains(childNodeType));
1✔
652
    }
653
    else if (parentNodeType == SIGNATURE_ELEMENT) {
1✔
654
      PsiElement signatureElement = parentNode.getPsi();
1✔
655
      LOG.assertTrue(signatureElement instanceof PerlSignatureElement);
1✔
656
      PsiElement declarationElement = ((PerlSignatureElement)signatureElement).getDeclarationElement();
1✔
657
      PsiElement defaultValueElement = ((PerlSignatureElement)signatureElement).getDefaultValueElement();
1✔
658
      if (defaultValueElement != null && declarationElement != null &&
1✔
659
          declarationElement.getStartOffsetInParent() < childNode.getStartOffsetInParent() &&
1✔
660
          (childNodeType == OPERATOR_ASSIGN) == commonCodeStyleSettings.PLACE_ASSIGNMENT_SIGN_ON_NEXT_LINE) {
661
        return getWrapBySettings(parentNode, commonCodeStyleSettings.ASSIGNMENT_WRAP, true);
1✔
662
      }
663
    }
1✔
664
    else if (parentNodeType == ATTRIBUTES && (childNodeType == COLON || isAttributeWithoutColon(childNode))) {
1✔
665
      return getWrapBySettings(parentNode, perlCodeStyleSettings.ATTRIBUTES_WRAP, false);
1✔
666
    }
667
    return null;
1✔
668
  }
669

670
  private boolean isAttributeWithoutColon(@NotNull ASTNode node) {
671
    return PsiUtilCore.getElementType(node) == ATTRIBUTE &&
1✔
672
           PsiUtilCore.getElementType(PerlPsiUtil.getPrevSignificantSibling(node.getPsi())) != COLON;
1✔
673
  }
674

675
  private @NotNull Wrap getWrapBySettings(@NotNull ASTNode parentNode, int settingsOption, boolean wrapFirst) {
676
    return getWrap(parentNode, getWrapType(settingsOption), wrapFirst);
1✔
677
  }
678

679
  private @NotNull WrapType getWrapType(int settingsOption) {
680
    if ((settingsOption & WRAP_ON_EVERY_ITEM) != 0) {
1✔
681
      return CHOP_DOWN_IF_LONG;
1✔
682
    }
683
    else if ((settingsOption & WRAP_ALWAYS) != 0) {
1✔
684
      return ALWAYS;
1✔
685
    }
686
    else if ((settingsOption & WRAP_AS_NEEDED) != 0) {
1✔
687
      return NORMAL;
1✔
688
    }
689
    return NONE;
1✔
690
  }
691

692
  private @NotNull Wrap getWrap(@NotNull ASTNode parentNode, @NotNull WrapType type, boolean wrapFirst) {
693
    return myWrapMap.computeIfAbsent(parentNode, key -> Wrap.createWrap(type, wrapFirst));
1✔
694
  }
695

696
  @Override
697
  public @Nullable Alignment getChildAlignment(@NotNull PerlAstBlock block, int newChildIndex) {
698
    ASTNode node = block.getNode();
1✔
699
    IElementType elementType = PsiUtilCore.getElementType(node);
1✔
700
    ASTNode parentNode = node == null ? null : node.getTreeParent();
1✔
701
    var commonCodeStyleSettings = getSettings();
1✔
702
    var perlCodeStyleSettings = getPerlSettings();
1✔
703

704
    if (perlCodeStyleSettings.ALIGN_CONSECUTIVE_ASSIGNMENTS != NO_ALIGN && elementType == SIGNATURE_ELEMENT) {
1✔
705
      IElementType lastChildNodeType = PsiUtilCore.getElementType(node.getLastChildNode());
1✔
706
      if (lastChildNodeType == OPERATOR_ASSIGN) {
1✔
UNCOV
707
        return null;
×
708
      }
709
      PsiElement psiElement = node.getPsi();
1✔
710
      assert psiElement instanceof PerlSignatureElement;
1✔
711
      if (((PerlSignatureElement)psiElement).hasDeclarationElement()) {
1✔
712
        if (perlCodeStyleSettings.ALIGN_CONSECUTIVE_ASSIGNMENTS == ALIGN_IN_STATEMENT) {
1✔
713
          return myAssignmentsElementAlignmentsMap.get(parentNode);
1✔
714
        }
715
        else if (perlCodeStyleSettings.ALIGN_CONSECUTIVE_ASSIGNMENTS == ALIGN_LINES) {
1✔
716
          return getLineBasedAlignment(node, myAssignmentsAlignmentsMap);
1✔
717
        }
718
      }
719
    }
720
    if (elementType == SIGNATURE_CONTENT) {
1✔
721
      return commonCodeStyleSettings.ALIGN_MULTILINE_PARAMETERS ? myElementsALignmentsMap.get(block.getNode()) : null;
1✔
722
    }
723
    if (elementType == ATTRIBUTES && perlCodeStyleSettings.ALIGN_ATTRIBUTES) {
1✔
724
      return myElementsALignmentsMap.get(node);
1✔
725
    }
726
    if (elementType == COMMA_SEQUENCE_EXPR) {
1✔
727
      IElementType parentNodeType = PsiUtilCore.getElementType(parentNode);
1✔
728
      if (parentNodeType == CALL_ARGUMENTS || parentNodeType == PARENTHESISED_CALL_ARGUMENTS) {
1✔
729
        if (commonCodeStyleSettings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS) {
1✔
730
          return myElementsALignmentsMap.get(node);
1✔
731
        }
732
      }
733
      return null;
1✔
734
    }
735

736
    // this is default algorythm from AbstractBlock#getFirstChildAlignment()
737
    List<Block> subBlocks = block.getSubBlocks();
1✔
738
    for (final Block subBlock : subBlocks) {
1✔
739
      if (ASTBlock.getElementType(subBlock) == COMMENT_LINE) {
1✔
740
        continue;
1✔
741
      }
742
      Alignment alignment = subBlock.getAlignment();
1✔
743
      if (alignment != null) {
1✔
UNCOV
744
        return alignment;
×
745
      }
746
    }
1✔
747

748
    // trying to get alignment from the last child if incomplete
749
    if (newChildIndex == subBlocks.size()) {
1✔
750
      Block lastBlock = ContainerUtil.getLastItem(subBlocks);
1✔
751
      if (lastBlock instanceof PerlAstBlock astBlock && lastBlock.isIncomplete()) {
1✔
UNCOV
752
        List<Block> lastBlockSubBlocks = lastBlock.getSubBlocks();
×
UNCOV
753
        return getChildAlignment(astBlock, lastBlockSubBlocks.size());
×
754
      }
755
    }
756

757
    return null;
1✔
758
  }
759

760
  public @Nullable Boolean isIncomplete(@NotNull PerlAstBlock block) {
761
    ASTNode blockNode = block.getNode();
1✔
762
    if (blockNode == null) {
1✔
UNCOV
763
      return null;
×
764
    }
765
    IElementType elementType = block.getElementType();
1✔
766
    if (elementType == ATTRIBUTES) {
1✔
767
      return true;
1✔
768
    }
769
    if (elementType == CALL_ARGUMENTS) {
1✔
770
      Block lastSubBlock = block.getLastSubBlock();
1✔
771
      return lastSubBlock != null && lastSubBlock.isIncomplete();
1✔
772
    }
773
    if (elementType == SIGNATURE_ELEMENT) {
1✔
774
      PsiElement psiElement = blockNode.getPsi();
1✔
775
      assert psiElement instanceof PerlSignatureElement;
1✔
776
      return !((PerlSignatureElement)psiElement).hasDefaultValueElement();
1✔
777
    }
778
    if (COMMA_LIKE_SEQUENCES.contains(elementType)) {
1✔
779
      IElementType lastNodeType = PsiUtilCore.getElementType(blockNode.getLastChildNode());
1✔
780
      if (lastNodeType == COMMA || lastNodeType == FAT_COMMA) {
1✔
781
        return true;
1✔
782
      }
783
      if (elementType == SIGNATURE_CONTENT) {
1✔
784
        Block lastSubBlock = block.getLastSubBlock();
1✔
785
        if (lastSubBlock != null && lastSubBlock.isIncomplete()) {
1✔
786
          return true;
1✔
787
        }
788
      }
789
    }
1✔
790
    else if (STATEMENTS.contains(elementType)) {
1✔
791
      PsiElement lastLeaf = PsiTreeUtil.getDeepestLast(blockNode.getPsi());
1✔
792
      return PsiUtilCore.getElementType(lastLeaf) != SEMICOLON;
1✔
793
    }
794
    else if (PsiUtilCore.getElementType(blockNode) == REPLACEMENT_REGEX) {
1✔
795
      return true;
1✔
796
    }
797

798
    return null;
1✔
799
  }
800

801
  /**
802
   * @return line number of the node or -1 if node is null or document missing
803
   */
804
  private int getNodeLine(@Nullable ASTNode node) {
805
    Document document = getDocument();
1✔
806
    return node == null || document == null ? -1 : document.getLineNumber(node.getStartOffset());
1✔
807
  }
808

809
  private boolean isLast(@NotNull Block block) {
810
    var astNode = ASTBlock.getNode(block);
1✔
811
    return astNode != null && FormatterUtil.getNextNonWhitespaceSibling(astNode) == null;
1✔
812
  }
813

814
  private boolean isFirst(@NotNull Block block) {
815
    var astNode = ASTBlock.getNode(block);
1✔
816
    return astNode != null && FormatterUtil.getPreviousNonWhitespaceSibling(astNode) == null;
1✔
817
  }
818
}
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