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

Camelcade / Perl5-IDEA / #525521559

30 May 2025 06:24AM UTC coverage: 82.248% (-0.03%) from 82.275%
#525521559

push

github

hurricup
Suppressed SameParameterValue warning

30866 of 37528 relevant lines covered (82.25%)

0.82 hits per line

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

99.04
/plugin/core/src/main/java/com/perl5/lang/perl/lexer/PerlBaseLexer.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.lexer;
18

19
import com.intellij.openapi.diagnostic.Logger;
20
import com.intellij.openapi.project.Project;
21
import com.intellij.openapi.util.AtomicNotNullLazyValue;
22
import com.intellij.openapi.util.Pair;
23
import com.intellij.openapi.util.Trinity;
24
import com.intellij.openapi.util.text.StringUtil;
25
import com.intellij.psi.TokenType;
26
import com.intellij.psi.tree.IElementType;
27
import com.intellij.psi.tree.TokenSet;
28
import com.perl5.lang.perl.extensions.parser.PerlParserExtension;
29
import com.perl5.lang.perl.idea.configuration.settings.PerlSharedSettings;
30
import com.perl5.lang.perl.idea.project.PerlNamesCache;
31
import com.perl5.lang.perl.parser.PerlParserImpl;
32
import com.perl5.lang.perl.parser.moose.MooseElementTypes;
33
import com.perl5.lang.perl.psi.PerlString;
34
import com.perl5.lang.perl.psi.references.PerlImplicitDeclarationsService;
35
import com.perl5.lang.perl.util.PerlPackageUtil;
36
import com.perl5.lang.perl.util.PerlPluginUtil;
37
import org.jetbrains.annotations.NotNull;
38
import org.jetbrains.annotations.Nullable;
39

40
import java.util.*;
41
import java.util.regex.Pattern;
42

43
import static com.perl5.lang.perl.lexer.PerlLexer.*;
44
import static com.perl5.lang.pod.parser.psi.PodSyntaxElements.CUT_COMMAND_WITH_LEADING_NEWLINE;
45

46

47
public abstract class PerlBaseLexer extends PerlProtoLexer implements PerlElementTypes,
48
                                                                      MooseElementTypes {
49
  private static final Logger LOG = Logger.getInstance(PerlBaseLexer.class);
1✔
50
  // fixme move somewhere
51
  public static final String STRING_UNDEF = "undef";
52

53
  private static final Pattern USE_TRYCATCH_PATTERN = Pattern.compile("use\\s+TryCatch");
1✔
54
  private Boolean myHasTryCatch = null;
1✔
55
  // this is used to lex single-quoted strings.
56
  private char mySingleOpenQuoteChar = 0;
1✔
57
  private char mySingleCloseQuoteChar = 0;
1✔
58

59
  private static final Map<IElementType, String> ALLOWED_REGEXP_MODIFIERS = Map.of(
1✔
60
    RESERVED_S, "nmsixpodualgcer",
61
    RESERVED_M, "nmsixpodualgc",
62
    RESERVED_QR, "nmsixpodual"
63
  );
64
  public static final String ALLOWED_TR_MODIFIERS = "cdsr";
65
  private static final List<IElementType> DQ_TOKENS = Arrays.asList(QUOTE_DOUBLE_OPEN, LP_STRING_QQ, QUOTE_DOUBLE_CLOSE, STRING_CONTENT_QQ);
1✔
66
  private static final List<IElementType> SQ_TOKENS = Arrays.asList(QUOTE_SINGLE_OPEN, LP_STRING_Q, QUOTE_SINGLE_CLOSE, STRING_CONTENT);
1✔
67
  private static final List<IElementType> XQ_TOKENS = Arrays.asList(QUOTE_TICK_OPEN, LP_STRING_QX, QUOTE_TICK_CLOSE, STRING_CONTENT_XQ);
1✔
68
  private static final List<IElementType> QW_TOKENS = Arrays.asList(QUOTE_SINGLE_OPEN, LP_STRING_QW, QUOTE_SINGLE_CLOSE);
1✔
69
  private static final List<IElementType> GLOB_TOKENS = Arrays.asList(LEFT_ANGLE, LP_STRING_QQ, RIGHT_ANGLE, STRING_CONTENT_QQ);
1✔
70

71
  private static final Map<IElementType, Trinity<IElementType, IElementType, IElementType>> SIGILS_TO_TOKENS_MAP = Map.of(
1✔
72
    SIGIL_SCALAR, Trinity.create(LEFT_BRACE_SCALAR, SCALAR_NAME, RIGHT_BRACE_SCALAR),
1✔
73
    SIGIL_SCALAR_INDEX, Trinity.create(LEFT_BRACE_SCALAR, SCALAR_NAME, RIGHT_BRACE_SCALAR),
1✔
74
    SIGIL_ARRAY, Trinity.create(LEFT_BRACE_ARRAY, ARRAY_NAME, RIGHT_BRACE_ARRAY),
1✔
75
    SIGIL_HASH, Trinity.create(LEFT_BRACE_HASH, HASH_NAME, RIGHT_BRACE_HASH),
1✔
76
    SIGIL_CODE, Trinity.create(LEFT_BRACE_CODE, SUB_NAME, RIGHT_BRACE_CODE),
1✔
77
    SIGIL_GLOB, Trinity.create(LEFT_BRACE_GLOB, GLOB_NAME, RIGHT_BRACE_GLOB)
1✔
78
  );
79

80
  protected static final String SUB_SIGNATURE = "Sub.Signature";
81

82
  private static final AtomicNotNullLazyValue<Boolean> ourListenersInitializer = AtomicNotNullLazyValue.createValue(()->{
1✔
83
    PerlParserExtension.EP_NAME.addChangeListener(PerlBaseLexer::refreshExtensions, PerlPluginUtil.getUnloadAwareDisposable());
1✔
84
    refreshExtensions();
1✔
85
    return true;
1✔
86
  });
87

88
  private static void refreshExtensions() {
89
    PerlParserImpl.restoreDefaultExtendsSet();
1✔
90

91
    for (PerlParserExtension extension : PerlParserExtension.EP_NAME.getExtensionList()) {
1✔
92
      // add extensions tokens
93
      List<Pair<IElementType, TokenSet>> extensionSets = extension.getExtensionSets();
1✔
94
      for (Pair<IElementType, TokenSet> extensionSet : extensionSets) {
1✔
95
        extendParserTokens(extensionSet.first, extensionSet.getSecond());
1✔
96
      }
1✔
97
    }
1✔
98
  }
1✔
99

100
  private static void extendParserTokens(IElementType setToExtend, TokenSet extendWith) {
101
    for (int i = 0; i < PerlParserImpl.EXTENDS_SETS_.length; i++) {
1✔
102
      if (PerlParserImpl.EXTENDS_SETS_[i].contains(setToExtend)) {
1✔
103
        PerlParserImpl.EXTENDS_SETS_[i] = TokenSet.orSet(PerlParserImpl.EXTENDS_SETS_[i], extendWith);
1✔
104
        break;
1✔
105
      }
106
    }
107
  }
1✔
108

109
  // last captured heredoc marker
110
  protected final Queue<PerlHeredocQueueElement> heredocQueue = new ArrayDeque<>(5);
1✔
111
  protected final PerlBracesStack myBracesStack = new PerlBracesStack();
1✔
112
  protected final PerlBracesStack myBracketsStack = new PerlBracesStack();
1✔
113
  protected final PerlBracesStack myParensStack = new PerlBracesStack();
1✔
114
  // flags that we are waiting for format on the next line
115
  protected boolean myFormatWaiting = false;
1✔
116
  // number of sections for the next regexp
117
  protected int sectionsNumber = 0;
1✔
118
  /**
119
   * Regex processor qr{} m{} s{}{}
120
   **/
121
  protected IElementType regexCommand = null;
1✔
122
  /**
123
   * Indicates that we did encounter {@code <<} and the next line may be a heredoc
124
   */
125
  private boolean myIsHeredocLike = false;
1✔
126
  private IElementType myCurrentSigilToken;
127
  private boolean myIsPerlSwitchEnabled = false;
1✔
128

129
  private @Nullable Project myProject;
130
  private AtomicNotNullLazyValue<Set<String>> mySubNamesProvider;
131
  private AtomicNotNullLazyValue<Set<String>> myNamespaceNamesProvider;
132
  private PerlImplicitDeclarationsService myImplicitSubsService;
133
  private final Set<String> myLocalPackages = new HashSet<>();
1✔
134

135
  public PerlBaseLexer() {
1✔
136
    ourListenersInitializer.get();
1✔
137
  }
1✔
138

139
  public PerlBaseLexer withProject(@Nullable Project project) {
140
    myProject = project;
1✔
141
    if (project != null) {
1✔
142
      myIsPerlSwitchEnabled = PerlSharedSettings.getInstance(project).PERL_SWITCH_ENABLED;
1✔
143
    }
144
    return this;
1✔
145
  }
146

147
  protected IElementType getPerlSwitchToken(IElementType token) {
148
    if (myIsPerlSwitchEnabled) {
1✔
149
      yybegin(YYINITIAL);
1✔
150
      return token;
1✔
151
    }
152
    yybegin(AFTER_IDENTIFIER);
1✔
153
    return SUB_NAME;
1✔
154
  }
155

156
  @Override
157
  public boolean isInitialState() {
158
    return super.isInitialState() &&
1✔
159
           !myFormatWaiting &&
160
           !isHeredocLike() &&
1✔
161
           heredocQueue.isEmpty() &&
1✔
162
           myBracesStack.isEmpty() &&
1✔
163
           myBracketsStack.isEmpty() &&
1✔
164
           myParensStack.isEmpty() &&
1✔
165
           mySingleOpenQuoteChar == 0;
166
  }
167

168
  /**
169
   * @apiNote use this method to set a quote charater for currently lexed single-quoted string content.
170
   */
171
  public void setSingleOpenQuoteChar(char singleOpenQuoteChar) {
172
    mySingleOpenQuoteChar = singleOpenQuoteChar;
1✔
173
    if (singleOpenQuoteChar != 0) {
1✔
174
      mySingleCloseQuoteChar = PerlString.getQuoteCloseChar(singleOpenQuoteChar);
1✔
175
    }
176
    else {
177
      mySingleCloseQuoteChar = 0;
1✔
178
    }
179
  }
1✔
180

181
  /**
182
   * We've met any sigil
183
   */
184
  protected IElementType startUnbracedVariable(@NotNull IElementType sigilToken) {
185
    return startUnbracedVariable(getRealLexicalState(), sigilToken);
1✔
186
  }
187

188
  /**
189
   * We've met any sigil
190
   */
191
  protected IElementType startUnbracedVariable(int state, @NotNull IElementType sigilToken) {
192
    myCurrentSigilToken = sigilToken;
1✔
193
    yybegin(state);
1✔
194
    pushStateAndBegin(VARIABLE_UNBRACED);
1✔
195
    return sigilToken;
1✔
196
  }
197

198
  /**
199
   * We've met one of the $ / [${]
200
   */
201
  @SuppressWarnings("SameReturnValue")
202
  protected IElementType processUnbracedScalarSigil() {
203
    myCurrentSigilToken = SIGIL_SCALAR;
1✔
204
    return SIGIL_SCALAR;
1✔
205
  }
206

207
  protected IElementType startBracedVariable() {
208
    Trinity<IElementType, IElementType, IElementType> sigilTokens = SIGILS_TO_TOKENS_MAP.get(myCurrentSigilToken);
1✔
209
    assert sigilTokens != null;
1✔
210
    myBracesStack.push(0, sigilTokens.third);
1✔
211
    return getLeftBrace(sigilTokens.first, VARIABLE_BRACED);
1✔
212
  }
213

214
  protected IElementType startBracedBlock() {
215
    return startBracedBlockWithState(YYINITIAL);
1✔
216
  }
217

218
  protected IElementType startBracedBlock(int returnState) {
219
    yybegin(returnState);
1✔
220
    return startBracedBlockWithState(YYINITIAL);
1✔
221
  }
222

223
  /**
224
   * Starts a braced block
225
   *
226
   * @param blockState state to start after brace
227
   * @return token type
228
   */
229
  protected IElementType startBracedBlockWithState(int blockState) {
230
    myBracesStack.push(0);
1✔
231
    pushState();
1✔
232
    return getLeftBrace(blockState);
1✔
233
  }
234

235
  protected IElementType getLeftBrace() {
236
    return getLeftBrace(YYINITIAL);
1✔
237
  }
238

239
  protected IElementType getLeftBrace(int newState) {
240
    return getLeftBrace(LEFT_BRACE, newState);
1✔
241
  }
242

243
  private IElementType getLeftBrace(IElementType braceType, int newState) {
244
    if (!myBracesStack.isEmpty()) {
1✔
245
      myBracesStack.incLast();
1✔
246
    }
247
    yybegin(newState);
1✔
248
    return braceType;
1✔
249
  }
250

251
  @SuppressWarnings("Duplicates")
252
  protected IElementType getRightBrace(int afterState) {
253
    if (!myBracesStack.isEmpty()) {
1✔
254
      if (myBracesStack.decLast() == 0) {
1✔
255
        Object userData = myBracesStack.peekAdditional();
1✔
256
        myBracesStack.pop();
1✔
257
        popState();
1✔
258
        return userData instanceof IElementType elementType ? elementType : RIGHT_BRACE;
1✔
259
      }
260
    }
261
    yybegin(afterState);
1✔
262
    return RIGHT_BRACE;
1✔
263
  }
264

265
  protected IElementType startBracketedBlock() {
266
    return startBracketedBlockWithState(YYINITIAL);
1✔
267
  }
268

269
  protected IElementType startBracketedBlock(int returnState) {
270
    yybegin(returnState);
1✔
271
    return startBracketedBlockWithState(YYINITIAL);
1✔
272
  }
273

274
  protected IElementType startBracketedBlockWithState(@SuppressWarnings("SameParameterValue") int blockState) {
275
    myBracketsStack.push(0);
1✔
276
    pushState();
1✔
277
    return getLeftBracket(blockState);
1✔
278
  }
279

280
  @SuppressWarnings("SameReturnValue")
281
  protected IElementType getLeftBracket(int newState) {
282
    if (!myBracketsStack.isEmpty()) {
1✔
283
      myBracketsStack.incLast();
1✔
284
    }
285
    yybegin(newState);
1✔
286
    return LEFT_BRACKET;
1✔
287
  }
288

289
  @SuppressWarnings({"Duplicates", "SameReturnValue"})
290
  protected IElementType getRightBracket(int afterState) {
291
    if (!myBracketsStack.isEmpty()) {
1✔
292
      if (myBracketsStack.decLast() == 0) {
1✔
293
        myBracketsStack.pop();
1✔
294
        popState();
1✔
295
        return RIGHT_BRACKET;
1✔
296
      }
297
    }
298
    yybegin(afterState);
1✔
299
    return RIGHT_BRACKET;
1✔
300
  }
301

302
  protected IElementType startParethesizedBlock(int afterState, int afterParenState) {
303
    return startParethesizedBlock(afterState, afterParenState, null);
1✔
304
  }
305

306
  protected IElementType startParethesizedBlock(int afterState, int afterParenState, Object userData) {
307
    myParensStack.push(0, userData);
1✔
308
    yybegin(afterState);
1✔
309
    pushState();
1✔
310
    return getLeftParen(afterParenState);
1✔
311
  }
312

313
  @SuppressWarnings("SameReturnValue")
314
  protected IElementType getLeftParen(int newState) {
315
    if (!myParensStack.isEmpty()) {
1✔
316
      myParensStack.incLast();
1✔
317
    }
318
    yybegin(newState);
1✔
319
    return LEFT_PAREN;
1✔
320
  }
321

322
  @SuppressWarnings({"Duplicates", "SameReturnValue"})
323
  protected IElementType getRightParen(int afterState) {
324
    if (!myParensStack.isEmpty()) {
1✔
325
      if (myParensStack.decLast() == 0) {
1✔
326
        myParensStack.pop();
1✔
327
        popState();
1✔
328
        return RIGHT_PAREN;
1✔
329
      }
330
    }
331
    yybegin(afterState);
1✔
332
    return RIGHT_PAREN;
1✔
333
  }
334

335
  /**
336
   * We've met identifier (variable name)
337
   */
338
  protected @NotNull IElementType getUnbracedVariableNameToken() {
339
    popState();
1✔
340
    char currentChar;
341
    if (
1✔
342
      !myParensStack.isEmpty() &&
1✔
343
      yylength() == 1 &&
1✔
344
      StringUtil.containsChar(",=)", currentChar = yytext().charAt(0)) &&
1✔
345
      myParensStack.peek() == 1 &&
1✔
346
      myParensStack.peekAdditional() == SUB_SIGNATURE) {
1✔
347
      if (currentChar == ',') {
1✔
348
        yybegin(YYINITIAL);
1✔
349
        return COMMA;
1✔
350
      }
351
      else if (currentChar == '=') {
1✔
352
        yybegin(YYINITIAL);
1✔
353
        return OPERATOR_ASSIGN;
1✔
354
      }
355
      else if (currentChar == ')') {
1✔
356
        return getRightParen(SUB_ATTRIBUTES);
1✔
357
      }
358
    }
359
    return getVariableNameTokenBySigil();
1✔
360
  }
361

362
  protected @NotNull IElementType getBracedVariableNameToken() {
363
    yybegin(YYINITIAL);
1✔
364
    return getVariableNameTokenBySigil();
1✔
365
  }
366

367
  private IElementType getVariableNameTokenBySigil() {
368
    IElementType nameToken = SIGILS_TO_TOKENS_MAP.get(myCurrentSigilToken).second;
1✔
369
    if (nameToken != SUB_NAME) {
1✔
370
      return nameToken;
1✔
371
    }
372

373
    CharSequence tokenText = yytext();
1✔
374
    int tokenLength = tokenText.length();
1✔
375
    if (tokenLength == 1) {
1✔
376
      return nameToken;
1✔
377
    }
378

379
    if (StringUtil.endsWithChar(tokenText, ':')) {
1✔
380
      return PACKAGE;
1✔
381
    }
382

383
    int nameIndex = StringUtil.lastIndexOfAny(tokenText, ":'") + 1;
1✔
384
    if (nameIndex == 0) {
1✔
385
      return nameToken;
1✔
386
    }
387

388
    yypushback(tokenLength - nameIndex);
1✔
389
    pushStateAndBegin(LEX_SUB_NAME);
1✔
390
    return QUALIFYING_PACKAGE;
1✔
391
  }
392

393
  @Override
394
  protected void resetInternals() {
395
    super.resetInternals();
1✔
396
    myBracesStack.clear();
1✔
397
    myBracketsStack.clear();
1✔
398
    myParensStack.clear();
1✔
399
    heredocQueue.clear();
1✔
400
    myFormatWaiting = false;
1✔
401
    setHeredocLike(false);
1✔
402
    myHasTryCatch = null;
1✔
403
    setSingleOpenQuoteChar((char)0);
1✔
404

405
    myImplicitSubsService = myProject == null ? null : PerlImplicitDeclarationsService.getInstance(myProject);
1✔
406

407
    mySubNamesProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
408
      assert myProject != null;
1✔
409
      return PerlNamesCache.getInstance(myProject).getSubsNamesSet();
1✔
410
    });
411

412
    myNamespaceNamesProvider = AtomicNotNullLazyValue.createValue(() -> {
1✔
413
      assert myProject != null;
1✔
414
      return PerlNamesCache.getInstance(myProject).getNamespacesNamesSet();
1✔
415
    });
416
    myLocalPackages.clear();
1✔
417
  }
1✔
418

419
  public void setHasTryCatch(Boolean hasTryCatch) {
420
    myHasTryCatch = hasTryCatch;
1✔
421
  }
1✔
422

423
  /**
424
   * Fast method for lexing spaces with or without newlines. Lexer invokes this method after lexing first space character.
425
   */
426
  @SuppressWarnings("SameReturnValue")
427
  protected final @NotNull IElementType captureSpaces() {
428
    var bufferEnd = getBufferEnd();
1✔
429
    var currentOffset = getTokenEnd() - 1;
1✔
430
    var buffer = getBuffer();
1✔
431
    while (currentOffset < bufferEnd) {
1✔
432
      var currentChar = buffer.charAt(currentOffset);
1✔
433
      if (currentChar == '\n') {
1✔
434
        setHeredocLike(false);
1✔
435
        if (myFormatWaiting) {
1✔
436
          currentOffset++;
1✔
437
          myFormatWaiting = false;
1✔
438
          yybegin(CAPTURE_FORMAT);
1✔
439
          break;
1✔
440
        }
441
        else if (!heredocQueue.isEmpty()) {
1✔
442
          currentOffset++;
1✔
443
          startHeredocCapture();
1✔
444
          break;
1✔
445
        }
446
      }
447
      else if (!Character.isWhitespace(currentChar)) {
1✔
448
        break;
1✔
449
      }
450
      currentOffset++;
1✔
451
    }
1✔
452
    setTokenEnd(currentOffset);
1✔
453
    return TokenType.WHITE_SPACE;
1✔
454
  }
455

456
  /**
457
   * Fast method for lexing line comment
458
   */
459
  @SuppressWarnings("SameReturnValue")
460
  protected final @NotNull IElementType captureComment() {
461
    var bufferEnd = getBufferEnd();
1✔
462
    var currentOffset = getTokenEnd();
1✔
463
    var buffer = getBuffer();
1✔
464
    while (currentOffset < bufferEnd) {
1✔
465
      if (buffer.charAt(currentOffset) == '\n') {
1✔
466
        break;
1✔
467
      }
468
      currentOffset++;
1✔
469
    }
470
    setTokenEnd(currentOffset);
1✔
471
    return COMMENT_LINE;
1✔
472
  }
473

474

475
  /**
476
   * Fast {@code __END__} block capture method. Invoked on the first character match after the block beginning.
477
   */
478
  @SuppressWarnings("SameReturnValue")
479
  protected final @NotNull IElementType captureEndBlock(){
480
    int offset = getTokenStart();
1✔
481
    var bufferEnd = getBufferEnd();
1✔
482
    var buffer = getBuffer();
1✔
483
    while( offset < bufferEnd){
1✔
484
      offset = StringUtil.indexOf(buffer, "\n=", offset) + 1;
1✔
485
      if( offset < 1){
1✔
486
        // no pod inside
487
        offset = bufferEnd;
1✔
488
        yybegin(YYINITIAL);
1✔
489
        break;
1✔
490
      }
491
      char nextChar = offset + 1 < bufferEnd ? buffer.charAt(offset + 1) : 0;
1✔
492
      if( nextChar >= 'a' && nextChar <= 'z' ||  nextChar >= 'A' && nextChar <= 'Z'){
1✔
493
        // valid pod starter
494
        break;
×
495
      }
496
      offset++;
1✔
497
    }
1✔
498
    setTokenEnd(offset);
1✔
499
    return COMMENT_BLOCK;
1✔
500
  }
501

502
  /**
503
   * Fast POD block capture method. Invoked after opening line was captured. No line beginning check was preformed.
504
   *
505
   * @param true iff pod block is not expecting to have end. Not sure why, but this was initial logic for the pod in the end blocks.
506
   */
507
  protected final IElementType capturePod(boolean isEndless) {
508
    var tokenStart = getTokenStart();
1✔
509
    var buffer = getBuffer();
1✔
510
    if (tokenStart != 0 && buffer.charAt(tokenStart - 1) != '\n') {
1✔
511
      yypushback(yylength() - 1);
1✔
512
      yybegin(YYINITIAL);
1✔
513
      return OPERATOR_ASSIGN;
1✔
514
    }
515

516
    int bufferEnd = getBufferEnd();
1✔
517
    if (isEndless) {
1✔
518
      setTokenEnd(bufferEnd);
1✔
519
      return POD;
1✔
520
    }
521

522
    int tokenEnd = getTokenEnd();
1✔
523
    while (tokenEnd < bufferEnd) {
1✔
524
      tokenEnd = StringUtil.indexOf(buffer, CUT_COMMAND_WITH_LEADING_NEWLINE, tokenEnd);
1✔
525
      if (tokenEnd < 0) {
1✔
526
        tokenEnd = bufferEnd;
1✔
527
        break;
1✔
528
      }
529
      tokenEnd += CUT_COMMAND_WITH_LEADING_NEWLINE.length();
1✔
530
      if (tokenEnd >= bufferEnd) {
1✔
531
        tokenEnd = bufferEnd;
1✔
532
        break;
1✔
533
      }
534
      if (!Character.isWhitespace(buffer.charAt(tokenEnd))) {
1✔
535
        continue;
1✔
536
      }
537
      tokenEnd = StringUtil.indexOf(buffer, '\n', tokenEnd);
1✔
538
      if (tokenEnd < 0) {
1✔
539
        tokenEnd = bufferEnd;
×
540
      }
541
      break;
542
    }
543
    setTokenEnd(tokenEnd);
1✔
544
    return POD;
1✔
545
  }
546

547
  /**
548
   * Distincts sub_name from qualified sub_name by :
549
   *
550
   * @return guessed token
551
   */
552
  protected @NotNull IElementType getIdentifierTokenWithoutIndex() {
553
    CharSequence tokenText = yytext();
1✔
554
    int lastIndex;
555
    if (StringUtil.endsWithChar(tokenText, ':')) {
1✔
556
      return PACKAGE;
1✔
557
    }
558

559
    int tokenLength = tokenText.length();
1✔
560
    if ((lastIndex = StringUtil.lastIndexOfAny(tokenText, ":'") + 1) > 0) {
1✔
561
      yypushback(tokenLength - lastIndex);
1✔
562
      pushStateAndBegin(LEX_SUB_NAME);
1✔
563
      return QUALIFYING_PACKAGE;
1✔
564
    }
565
    return SUB_NAME;
1✔
566
  }
567

568
  /**
569
   * Bareword parser, resolves built-ins and runs additional processings where it's necessary
570
   *
571
   * @return token type
572
   */
573
  protected IElementType getIdentifierToken() {
574
    String tokenText = yytext().toString();
1✔
575
    IElementType tokenType;
576

577
    if (StringUtil.endsWithChar(tokenText, ':')) {
1✔
578
      tokenType = PACKAGE;
1✔
579
    }
580
    else if (myProject != null) {
1✔
581
      String canonicalName = PerlPackageUtil.getCanonicalName(tokenText);
1✔
582
      if (!StringUtil.containsChar(canonicalName, ':')) {
1✔
583
        if (StringUtil.isCapitalized(canonicalName) &&
1✔
584
            (myNamespaceNamesProvider.getValue().contains(canonicalName) || myLocalPackages.contains(canonicalName))) {
1✔
585
          tokenType = PACKAGE;
1✔
586
        }
587
        else {
588
          tokenType = SUB_NAME;
1✔
589
        }
590
      }
591
      else if (StringUtil.equals(canonicalName, "UNIVERSAL::can")) {
1✔
592
        tokenType = QUALIFYING_PACKAGE;
1✔
593
      }
594
      else if (myImplicitSubsService.getSub(canonicalName) != null) {
1✔
595
        tokenType = QUALIFYING_PACKAGE;
1✔
596
      }
597
      else if (mySubNamesProvider.getValue().contains(canonicalName)) {
1✔
598
        tokenType = QUALIFYING_PACKAGE;
1✔
599
      }
600
      else if (myNamespaceNamesProvider.getValue().contains(canonicalName) || myLocalPackages.contains(canonicalName)) {
1✔
601
        tokenType = PACKAGE;
1✔
602
      }
603
      else {
604
        tokenType = QUALIFYING_PACKAGE;
1✔
605
      }
606
    }
1✔
607
    else {    // fallback for words scanner
608
      tokenType = IDENTIFIER;
1✔
609
    }
610

611
    yybegin(AFTER_IDENTIFIER);
1✔
612

613
    if (tokenType == QUALIFYING_PACKAGE) {
1✔
614
      int lastIndex = StringUtil.lastIndexOfAny(tokenText, ":'") + 1;
1✔
615
      yypushback(tokenText.length() - lastIndex);
1✔
616
      pushStateAndBegin(LEX_SUB_NAME);
1✔
617
    }
618

619
    return tokenType;
1✔
620
  }
621

622
  private List<IElementType> getStringTokens() {
623
    int currentState = getRealLexicalState();
1✔
624

625
    if (currentState == QUOTE_LIKE_OPENER_Q || currentState == QUOTE_LIKE_OPENER_Q_NOSHARP) {
1✔
626
      return SQ_TOKENS;
1✔
627
    }
628
    if (currentState == QUOTE_LIKE_OPENER_QQ || currentState == QUOTE_LIKE_OPENER_QQ_NOSHARP) {
1✔
629
      return DQ_TOKENS;
1✔
630
    }
631
    if (currentState == QUOTE_LIKE_OPENER_QX || currentState == QUOTE_LIKE_OPENER_QX_NOSHARP) {
1✔
632
      return XQ_TOKENS;
1✔
633
    }
634
    if (currentState == QUOTE_LIKE_OPENER_QW || currentState == QUOTE_LIKE_OPENER_QW_NOSHARP) {
1✔
635
      return QW_TOKENS;
1✔
636
    }
637
    if (currentState == QUOTE_LIKE_OPENER_GLOB) {
1✔
638
      return GLOB_TOKENS;
1✔
639
    }
640

641
    throw new RuntimeException("Unknown lexical state for string token " + currentState);
×
642
  }
643

644
  /**
645
   * Finds opening quote, body and close quote of the quote-like structure. Pushes them as pre-parsed tokens.
646
   *
647
   * @return token of the opening quote for the string.
648
   * @see #captureRegex()
649
   * @see #captureTr()
650
   */
651
  protected IElementType captureString() {
652
    CharSequence buffer = getBuffer();
1✔
653
    int currentPosition = getTokenStart();
1✔
654
    int bufferEnd = getBufferEnd();
1✔
655

656
    char openQuote = buffer.charAt(currentPosition);
1✔
657

658
    List<IElementType> stringTokens = getStringTokens();
1✔
659
    pushPreparsedToken(currentPosition++, currentPosition, stringTokens.getFirst());
1✔
660

661
    int contentStart = currentPosition;
1✔
662
    boolean hasEscape = false;
1✔
663
    boolean hasSigil = false;
1✔
664

665
    boolean isEscaped = false;
1✔
666
    int quotesDepth = 0;
1✔
667
    char closeQuote = PerlString.getQuoteCloseChar(openQuote);
1✔
668
    boolean quotesDiffer = openQuote != closeQuote;
1✔
669

670
    while (currentPosition < bufferEnd) {
1✔
671
      char currentChar = buffer.charAt(currentPosition);
1✔
672

673
      if (!isEscaped && quotesDepth == 0 && currentChar == closeQuote) {
1✔
674
        break;
1✔
675
      }
676

677
      //noinspection Duplicates
678
      if (!isEscaped && quotesDiffer) {
1✔
679
        if (currentChar == openQuote) {
1✔
680
          quotesDepth++;
1✔
681
        }
682
        else if (currentChar == closeQuote) {
1✔
683
          quotesDepth--;
1✔
684
        }
685
      }
686

687
      isEscaped = !isEscaped && currentChar == '\\';
1✔
688
      hasEscape = hasEscape || currentChar == '\\';
1✔
689
      hasSigil = hasSigil || currentChar == '$' || currentChar == '@';
1✔
690

691
      currentPosition++;
1✔
692
    }
1✔
693

694
    if (currentPosition > contentStart) {
1✔
695
      IElementType contentTokenType = stringTokens.get(1);
1✔
696
      if (contentTokenType == LP_STRING_Q && !hasEscape) {
1✔
697
        contentTokenType = stringTokens.get(3);
1✔
698
      }
699
      else if (contentTokenType == LP_STRING_QX && openQuote == '\'') {
1✔
700
        contentTokenType = LP_STRING_QX_RESTRICTED;
1✔
701
      }
702
      else if ((contentTokenType == LP_STRING_QQ || contentTokenType == LP_STRING_QX) && !hasEscape && !hasSigil) {
1✔
703
        contentTokenType = stringTokens.get(3);
1✔
704
      }
705

706
      pushPreparsedToken(contentStart, currentPosition, contentTokenType);
1✔
707
    }
708

709
    if (currentPosition < bufferEnd) {
1✔
710
      // got close quote
711
      pushPreparsedToken(currentPosition++, currentPosition, stringTokens.get(2));
1✔
712
    }
713
    popState();
1✔
714
    return getPreParsedToken();
1✔
715
  }
716

717
  /**
718
   * Parsing tr/y content
719
   *
720
   * @return first token
721
   */
722
  public IElementType captureTr() {
723
    popState();
1✔
724
    yybegin(AFTER_VALUE);
1✔
725
    CharSequence buffer = getBuffer();
1✔
726
    int currentOffset = getTokenStart();
1✔
727
    int bufferEnd = getBufferEnd();
1✔
728

729
    // search block
730
    char openQuote = buffer.charAt(currentOffset);
1✔
731
    char closeQuote = PerlString.getQuoteCloseChar(openQuote);
1✔
732
    boolean quotesDiffer = openQuote != closeQuote;
1✔
733
    pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_OPEN);
1✔
734

735
    currentOffset = parseTrBlockContent(currentOffset, openQuote, closeQuote);
1✔
736

737
    // close quote
738
    if (currentOffset < bufferEnd) {
1✔
739
      pushPreparsedToken(currentOffset++, currentOffset, quotesDiffer ? REGEX_QUOTE_CLOSE : REGEX_QUOTE);
1✔
740
    }
741

742
    // between blocks
743
    if (quotesDiffer) {
1✔
744
      currentOffset = lexWhiteSpacesAndComments(currentOffset);
1✔
745
    }
746

747
    // second block
748
    if (currentOffset < bufferEnd) {
1✔
749
      if (quotesDiffer) {
1✔
750
        openQuote = buffer.charAt(currentOffset);
1✔
751
        closeQuote = PerlString.getQuoteCloseChar(openQuote);
1✔
752
        pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_OPEN);
1✔
753
      }
754

755
      currentOffset = parseTrBlockContent(currentOffset, openQuote, closeQuote);
1✔
756
    }
757

758
    // close quote
759
    if (currentOffset < bufferEnd) {
1✔
760
      pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_CLOSE);
1✔
761
    }
762

763

764
    // trans modifiers
765
    if (currentOffset < bufferEnd) {
1✔
766
      int blockStart = currentOffset;
1✔
767
      while (currentOffset < bufferEnd && StringUtil.containsChar(ALLOWED_TR_MODIFIERS, buffer.charAt(currentOffset))) {
1✔
768
        currentOffset++;
1✔
769
      }
770

771
      if (blockStart < currentOffset) {
1✔
772
        pushPreparsedToken(blockStart, currentOffset, REGEX_MODIFIER);
1✔
773
      }
774
    }
775

776
    return getPreParsedToken();
1✔
777
  }
778

779
  /**
780
   * Parsing tr block content till close quote
781
   *
782
   * @param currentOffset start offset
783
   * @param closeQuote    close quote character
784
   * @return next offset
785
   */
786
  private int parseTrBlockContent(int currentOffset, char openQuote, char closeQuote) {
787
    int blockStartOffset = currentOffset;
1✔
788
    CharSequence buffer = getBuffer();
1✔
789
    int bufferEnd = getBufferEnd();
1✔
790
    boolean isEscaped = false;
1✔
791
    boolean isQuoteDiffers = openQuote != closeQuote;
1✔
792
    int quotesLevel = 0;
1✔
793

794
    while (currentOffset < bufferEnd) {
1✔
795
      char currentChar = buffer.charAt(currentOffset);
1✔
796

797
      if (!isEscaped && quotesLevel == 0 && currentChar == closeQuote) {
1✔
798
        if (currentOffset > blockStartOffset) {
1✔
799
          pushPreparsedToken(blockStartOffset, currentOffset, openQuote == '\'' ? LP_STRING_QQ_RESTRICTED : LP_STRING_TR);
1✔
800
        }
801
        break;
802
      }
803
      //noinspection Duplicates
804
      if (isQuoteDiffers && !isEscaped) {
1✔
805
        if (currentChar == openQuote) {
1✔
806
          quotesLevel++;
1✔
807
        }
808
        else if (currentChar == closeQuote) {
1✔
809
          quotesLevel--;
1✔
810
        }
811
      }
812

813
      isEscaped = (currentChar == '\\' && !isEscaped);
1✔
814
      currentOffset++;
1✔
815
    }
1✔
816

817
    return currentOffset;
1✔
818
  }
819

820
  /**
821
   * Lexing empty spaces and comments between regex/tr blocks and adding tokens to the target list
822
   *
823
   * @param currentOffset start offset
824
   * @return new offset
825
   */
826
  protected int lexWhiteSpacesAndComments(int currentOffset) {
827
    CharSequence buffer = getBuffer();
1✔
828
    int bufferEnd = getBufferEnd();
1✔
829
    while (currentOffset < bufferEnd) {
1✔
830
      char currentChar = buffer.charAt(currentOffset);
1✔
831

832
      if (currentChar == '\n') {
1✔
833
        // fixme check heredocs ?
834
        pushPreparsedToken(currentOffset++, currentOffset, TokenType.WHITE_SPACE);
1✔
835
      }
836
      else if (Character.isWhitespace(currentChar))    // white spaces
1✔
837
      {
838
        int whiteSpaceStart = currentOffset;
1✔
839
        while (currentOffset < bufferEnd && Character.isWhitespace(currentChar = buffer.charAt(currentOffset)) && currentChar != '\n') {
1✔
840
          currentOffset++;
1✔
841
        }
842
        pushPreparsedToken(whiteSpaceStart, currentOffset, TokenType.WHITE_SPACE);
1✔
843
      }
1✔
844
      else if (currentChar == '#')    // line comment
1✔
845
      {
846
        int commentStart = currentOffset;
1✔
847
        while (currentOffset < bufferEnd && buffer.charAt(currentOffset) != '\n') {
1✔
848
          currentOffset++;
1✔
849
        }
850
        pushPreparsedToken(getCustomToken(commentStart, currentOffset, COMMENT_LINE));
1✔
851
      }
852
      else {
853
        break;
854
      }
855
    }
1✔
856

857
    return currentOffset;
1✔
858
  }
859

860
  public int getRegexBlockEndOffset(int startOffset, char openingChar) {
861
    char closingChar = PerlString.getQuoteCloseChar(openingChar);
1✔
862
    CharSequence buffer = getBuffer();
1✔
863
    int bufferEnd = getBufferEnd();
1✔
864

865
    boolean isEscaped = false;
1✔
866
    boolean isQuotesDiffers = closingChar != openingChar;
1✔
867

868
    int delimiterLevel = 0;
1✔
869

870
    int currentOffset = startOffset;
1✔
871

872
    while (currentOffset < bufferEnd) {
1✔
873

874
      char currentChar = buffer.charAt(currentOffset);
1✔
875

876
      if (delimiterLevel == 0 && !isEscaped && closingChar == currentChar) {
1✔
877
        return currentOffset;
1✔
878
      }
879

880
      if (!isEscaped && isQuotesDiffers) {
1✔
881
        if (currentChar == openingChar) {
1✔
882
          delimiterLevel++;
1✔
883
        }
884
        else if (currentChar == closingChar && delimiterLevel > 0) {
1✔
885
          delimiterLevel--;
1✔
886
        }
887
      }
888

889
      isEscaped = !isEscaped && closingChar != '\\' && currentChar == '\\';
1✔
890

891
      currentOffset++;
1✔
892
    }
1✔
893
    return currentOffset;
1✔
894
  }
895

896
  /**
897
   * Parses regexp from the current position (opening delimiter) and preserves tokens in preparsedTokensList
898
   * REGEX_MODIFIERS = [msixpodualgcerxx]
899
   *
900
   * @return opening delimiter type of the regular expressions
901
   * @see #captureTr()
902
   * @see #captureString()
903
   */
904
  public IElementType captureRegex() {
905
    popState();
1✔
906
    yybegin(AFTER_VALUE);
1✔
907
    CharSequence buffer = getBuffer();
1✔
908
    int currentOffset = getTokenStart();
1✔
909
    int bufferEnd = getBufferEnd();
1✔
910

911
    char firstBlockOpeningQuote = buffer.charAt(currentOffset);
1✔
912
    boolean isInterpolationAllowedInMatch = firstBlockOpeningQuote != '\'';
1✔
913
    pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_OPEN);
1✔
914

915
    // find block 1
916
    int firstBlockEndOffset = getRegexBlockEndOffset(currentOffset, firstBlockOpeningQuote);
1✔
917
    CustomToken firstBlockToken = null;
1✔
918
    if (firstBlockEndOffset > currentOffset) {
1✔
919
      firstBlockToken = new CustomToken(currentOffset, firstBlockEndOffset, isInterpolationAllowedInMatch ? LP_REGEX : LP_REGEX_SQ);
1✔
920
      pushPreparsedToken(firstBlockToken);
1✔
921
    }
922

923
    currentOffset = firstBlockEndOffset;
1✔
924

925
    // find block 2
926
    CustomToken secondBlockOpeningToken = null;
1✔
927
    CustomToken secondBlockToken = null;
1✔
928

929
    if (currentOffset < bufferEnd) {
1✔
930
      if (sectionsNumber == 1) {
1✔
931
        pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_CLOSE);
1✔
932
      }
933
      else // should have second part
934
      {
935
        char secondBlockOpeningQuote = firstBlockOpeningQuote;
1✔
936
        if (firstBlockOpeningQuote == PerlString.getQuoteCloseChar(firstBlockOpeningQuote)) {
1✔
937
          secondBlockOpeningToken = new CustomToken(currentOffset++, currentOffset, REGEX_QUOTE);
1✔
938
          pushPreparsedToken(secondBlockOpeningToken);
1✔
939
        }
940
        else {
941
          pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_CLOSE);
1✔
942
          currentOffset = lexWhiteSpacesAndComments(currentOffset);
1✔
943

944
          if (currentOffset < bufferEnd) {
1✔
945
            secondBlockOpeningQuote = buffer.charAt(currentOffset);
1✔
946
            secondBlockOpeningToken = new CustomToken(currentOffset++, currentOffset, REGEX_QUOTE_OPEN);
1✔
947
            pushPreparsedToken(secondBlockOpeningToken);
1✔
948
          }
949
        }
950

951
        if (currentOffset < bufferEnd) {
1✔
952
          int secondBlockEndOffset = getRegexBlockEndOffset(currentOffset, secondBlockOpeningQuote);
1✔
953

954
          if (secondBlockEndOffset > currentOffset) {
1✔
955
            secondBlockToken = new CustomToken(
1✔
956
              currentOffset, secondBlockEndOffset, secondBlockOpeningQuote == '\'' ? LP_STRING_QQ_RESTRICTED : LP_STRING_RE);
1✔
957
            pushPreparsedToken(secondBlockToken);
1✔
958
            currentOffset = secondBlockEndOffset;
1✔
959
          }
960
        }
961

962
        if (currentOffset < bufferEnd) {
1✔
963
          pushPreparsedToken(currentOffset++, currentOffset, REGEX_QUOTE_CLOSE);
1✔
964
        }
965
      }
966
    }
967

968
    // check modifiers for x
969
    assert regexCommand != null;
1✔
970
    String allowedModifiers = ALLOWED_REGEXP_MODIFIERS.get(regexCommand);
1✔
971

972
    while (currentOffset < bufferEnd) {
1✔
973
      char currentChar = buffer.charAt(currentOffset);
1✔
974
      int modifierEndOffset = currentOffset + 1;
1✔
975
      if (!StringUtil.containsChar(allowedModifiers, currentChar))    // unknown modifier
1✔
976
      {
977
        break;
1✔
978
      }
979
      else if (currentChar == 'a' && modifierEndOffset < bufferEnd && buffer.charAt(modifierEndOffset) == 'a') {
1✔
980
        modifierEndOffset++;
1✔
981
      }
982
      else if (currentChar == 'x')    // mark as extended
1✔
983
      {
984
        if (firstBlockToken != null) {
1✔
985
          if (modifierEndOffset < bufferEnd && buffer.charAt(modifierEndOffset) == 'x') {
1✔
986
            firstBlockToken.setTokenType(isInterpolationAllowedInMatch ? LP_REGEX_XX : LP_REGEX_XX_SQ);
1✔
987
            modifierEndOffset++;
1✔
988
          }
989
          else {
990
            firstBlockToken.setTokenType(isInterpolationAllowedInMatch ? LP_REGEX_X : LP_REGEX_X_SQ);
1✔
991
          }
992
        }
993
      }
994
      else if (currentChar == 'e')    // mark as evaluated
1✔
995
      {
996
        if (modifierEndOffset < bufferEnd && buffer.charAt(modifierEndOffset) == 'e') {
1✔
997
          modifierEndOffset++;
1✔
998
        }
999
        if (secondBlockOpeningToken != null) {
1✔
1000
          IElementType secondBlockOpeningTokenType = secondBlockOpeningToken.getTokenType();
1✔
1001
          if (secondBlockOpeningTokenType == REGEX_QUOTE_OPEN || secondBlockOpeningTokenType == REGEX_QUOTE_OPEN_E) {
1✔
1002
            secondBlockOpeningToken.setTokenType(REGEX_QUOTE_OPEN_E);
1✔
1003
          }
1004
          else if (secondBlockOpeningTokenType == REGEX_QUOTE || secondBlockOpeningTokenType == REGEX_QUOTE_E) {
1✔
1005
            secondBlockOpeningToken.setTokenType(REGEX_QUOTE_E);
1✔
1006
          }
1007
          else {
1008
            throw new RuntimeException("Bug, got: " + secondBlockOpeningTokenType);
×
1009
          }
1010
        }
1011
        if (secondBlockToken != null) {
1✔
1012
          secondBlockToken.setTokenType(getLPCodeBlockElementType());
1✔
1013
        }
1014
      }
1015

1016
      pushPreparsedToken(currentOffset, modifierEndOffset, REGEX_MODIFIER);
1✔
1017
      currentOffset = modifierEndOffset;
1✔
1018
    }
1✔
1019

1020
    return getPreParsedToken();
1✔
1021
  }
1022

1023
  protected void startHeredocCapture() {
1024
    pushState();
1✔
1025
    var headElement = heredocQueue.peek();
1✔
1026
    LOG.assertTrue(headElement != null);
1✔
1027
    if (!headElement.getMarker().isEmpty()) {
1✔
1028
      yybegin(CAPTURE_HEREDOC);
1✔
1029
    }
1030
    else {
1031
      yybegin(CAPTURE_HEREDOC_WITH_EMPTY_MARKER);
1✔
1032
    }
1033
  }
1✔
1034

1035
  /**
1036
   * Fast here-document capture method. Invoked on the first character after the newline
1037
   */
1038
  protected final @NotNull IElementType captureHeredoc(){
1039
    final int bodyStartOffset = getTokenStart();
1✔
1040
    int offset = bodyStartOffset;
1✔
1041
    var buffer = getBuffer();
1✔
1042
    var bufferEnd = getBufferEnd();
1✔
1043
    PerlHeredocQueueElement currentHeredoc = heredocQueue.poll();
1✔
1044
    popState();
1✔
1045
    LOG.assertTrue(currentHeredoc != null);
1✔
1046

1047
    var closeMarker = currentHeredoc.getMarker();
1✔
1048

1049
    while( offset < bufferEnd){
1✔
1050
      int markerStartOffset = StringUtil.indexOf(buffer, closeMarker, offset);
1✔
1051
      if (markerStartOffset < 0 || markerStartOffset >= bufferEnd) {
1✔
1052
        // no end marker text ahead
1053
        break;
1✔
1054
      }
1055

1056
      int markerEndOffset = markerStartOffset + closeMarker.length();
1✔
1057
      if( markerEndOffset < bufferEnd && buffer.charAt(markerEndOffset) != '\n' ){
1✔
1058
        // no newline or end of the buffer after end marker
1059
        offset = markerEndOffset;
1✔
1060
        continue;
1✔
1061
      }
1062

1063
      int spacesStartOffset = markerStartOffset;
1✔
1064
      if( currentHeredoc.isIndentable()){
1✔
1065
        // looking for leading spaces for indented heredoc
1066
        while( spacesStartOffset > 0 ){
1✔
1067
          char prevChar = buffer.charAt(spacesStartOffset-1);
1✔
1068
          if( prevChar == '\n'){
1✔
1069
            break;
1✔
1070
          }
1071
          if( !Character.isWhitespace(prevChar)){
1✔
1072
            break;
1✔
1073
          }
1074
          spacesStartOffset--;
1✔
1075
        }
1✔
1076
      }
1077

1078
      if( spacesStartOffset > 0 && buffer.charAt(spacesStartOffset-1) == '\n'){
1✔
1079
        // we found the valid end
1080
        if( spacesStartOffset > bodyStartOffset){
1✔
1081
          pushPreparsedToken(bodyStartOffset, spacesStartOffset, currentHeredoc.getTargetElement());
1✔
1082
        }
1083
        if( spacesStartOffset < markerStartOffset){
1✔
1084
          pushPreparsedToken(spacesStartOffset, markerStartOffset, TokenType.WHITE_SPACE);
1✔
1085
        }
1086
        pushPreparsedToken(markerStartOffset, markerEndOffset, currentHeredoc.getTerminatorElementType());
1✔
1087
        return getPreParsedToken();
1✔
1088
      }
1089
      offset = markerEndOffset;
1✔
1090
    }
1✔
1091

1092
    yybegin(YYINITIAL);
1✔
1093
    setTokenEnd(bufferEnd);
1✔
1094
    return currentHeredoc.getTargetElement();
1✔
1095
  }
1096

1097
  /**
1098
   * Fast here-document capture method with empty close marker. Invoked on the first character after the newline
1099
   */
1100
  protected final @NotNull IElementType captureHeredocWithEmptyMarker(){
1101
    final int bodyStartOffset = getTokenStart();
1✔
1102
    var buffer = getBuffer();
1✔
1103
    PerlHeredocQueueElement currentHeredoc = heredocQueue.poll();
1✔
1104
    LOG.assertTrue(currentHeredoc != null);
1✔
1105
    popState();
1✔
1106

1107
    if( buffer.charAt(bodyStartOffset) == '\n'){
1✔
1108
      // empty heredoc with empty marker
1109
      if( !heredocQueue.isEmpty()){
1✔
1110
        startHeredocCapture();
1✔
1111
      }
1112
      return currentHeredoc.getTerminatorElementType();
1✔
1113
    }
1114

1115
    var bufferEnd = getBufferEnd();
1✔
1116

1117
    int markerStartOffset = StringUtil.indexOf(buffer, "\n\n", bodyStartOffset) + 1;
1✔
1118
    if (markerStartOffset < 1) {
1✔
1119
      // no end marker text ahead
1120
      yybegin(YYINITIAL);
1✔
1121
      setTokenEnd(bufferEnd);
1✔
1122
      return currentHeredoc.getTargetElement();
1✔
1123
    }
1124

1125
    int markerEndOffset = markerStartOffset + 1;
1✔
1126

1127
    pushPreparsedToken(bodyStartOffset, markerStartOffset, currentHeredoc.getTargetElement());
1✔
1128
    pushPreparsedToken(markerStartOffset, markerEndOffset, currentHeredoc.getTerminatorElementType());
1✔
1129
    if( !heredocQueue.isEmpty()){
1✔
1130
      startHeredocCapture();
1✔
1131
    }
1132

1133
    return getPreParsedToken();
1✔
1134
  }
1135

1136

1137
  protected IElementType registerPackage(IElementType tokenType) {
1138
    myLocalPackages.add(PerlPackageUtil.getCanonicalNamespaceName(yytext().toString()));
1✔
1139
    return tokenType;
1✔
1140
  }
1141

1142
  /**
1143
   * Changes current state to nosharp one if necessary
1144
   */
1145
  protected void setNoSharpState() {
1146
    int realLexicalState = getRealLexicalState();
1✔
1147
    if (realLexicalState == QUOTE_LIKE_OPENER_Q) {
1✔
1148
      yybegin(QUOTE_LIKE_OPENER_Q_NOSHARP);
1✔
1149
    }
1150
    else if (realLexicalState == QUOTE_LIKE_OPENER_QQ) {
1✔
1151
      yybegin(QUOTE_LIKE_OPENER_QQ_NOSHARP);
1✔
1152
    }
1153
    else if (realLexicalState == QUOTE_LIKE_OPENER_QX) {
1✔
1154
      yybegin(QUOTE_LIKE_OPENER_QX_NOSHARP);
1✔
1155
    }
1156
    else if (realLexicalState == QUOTE_LIKE_OPENER_QW) {
1✔
1157
      yybegin(QUOTE_LIKE_OPENER_QW_NOSHARP);
1✔
1158
    }
1159
    else if (realLexicalState == TRANS_OPENER) {
1✔
1160
      yybegin(TRANS_OPENER_NO_SHARP);
1✔
1161
    }
1162
    else if (realLexicalState == REGEX_OPENER) {
1✔
1163
      yybegin(REGEX_OPENER_NO_SHARP);
1✔
1164
    }
1165
  }
1✔
1166

1167
  /**
1168
   * Captures quoted here-doc opener after <<[~] operator. Pushes heredoc data into queue
1169
   *
1170
   * @param heredocElementType element type for heredoc body
1171
   * @param stringOpenerState  state for string capture
1172
   * @return next string token type
1173
   */
1174
  public IElementType captureQuotedHeredocMarker(IElementType heredocElementType, int stringOpenerState, boolean isIndentable) {
1175
    yybegin(AFTER_VALUE);
1✔
1176
    pushState();
1✔
1177
    heredocQueue.add(new PerlHeredocQueueElement(heredocElementType, yytext().subSequence(1, yylength() - 1), isIndentable));
1✔
1178
    yybegin(stringOpenerState);
1✔
1179
    return captureString();
1✔
1180
  }
1181

1182
  /**
1183
   * Captures bare here-doc opener after <<[~] operator. Pushes heredoc data into queue
1184
   *
1185
   * @param heredocElementType element type for heredoc body
1186
   * @return string token type
1187
   */
1188
  @SuppressWarnings("SameReturnValue")
1189
  public IElementType captureBareHeredocMarker(IElementType heredocElementType, boolean isIndentable) {
1190
    yybegin(AFTER_VALUE);
1✔
1191
    heredocQueue.add(new PerlHeredocQueueElement(heredocElementType, yytext(), isIndentable));
1✔
1192
    return STRING_CONTENT;
1✔
1193
  }
1194

1195
  public boolean isHeredocLike() {
1196
    return myIsHeredocLike;
1✔
1197
  }
1198

1199
  public void setHeredocLike(boolean heredocLike) {
1200
    myIsHeredocLike = heredocLike;
1✔
1201
  }
1✔
1202

1203
  /**
1204
   * Registers used package if necessary. Used for TryCatch logic for now
1205
   *
1206
   * @return PACKAGE element type
1207
   */
1208
  @SuppressWarnings("SameReturnValue")
1209
  protected IElementType registerUse() {
1210
    if (StringUtil.equals(yytext(), "TryCatch")) {
1✔
1211
      myHasTryCatch = true;
1✔
1212
    }
1213
    return PACKAGE;
1✔
1214
  }
1215

1216
  /**
1217
   * @return true if we are suppose to use TryCatch compounds
1218
   * Logic of detection is highly optimized and put some restrictions:
1219
   * buffer should explicitly contains {@code use TryCatch;} before first try/catch
1220
   */
1221
  protected boolean hasTryCatch() {
1222
    if (myHasTryCatch != null) {
1✔
1223
      return myHasTryCatch;
1✔
1224
    }
1225
    if (getBufferStart() == 0) {
1✔
1226
      return myHasTryCatch = false;
1✔
1227
    }
1228
    return myHasTryCatch = USE_TRYCATCH_PATTERN.matcher(getBuffer().subSequence(0, getBufferStart())).find();
×
1229
  }
1230

1231
  /**
1232
   * Handles try token depending on TryCatch usege
1233
   */
1234
  protected IElementType handleTry() {
1235
    if (hasTryCatch()) {
1✔
1236
      pushback();
1✔
1237
      yybegin(BEFORE_TRY_TRYCATCH);
1✔
1238
      return RESERVED_TRYCATCH;
1✔
1239
    }
1240
    else {
1241
      yybegin(AFTER_TRY);
1✔
1242
      return RESERVED_TRY;
1✔
1243
    }
1244
  }
1245

1246
  public @NotNull IElementType getLPCodeBlockElementType() {
1247
    return hasTryCatch() ? LP_CODE_BLOCK_WITH_TRYCATCH : LP_CODE_BLOCK;
1✔
1248
  }
1249

1250
  /**
1251
   * @return true iff current token ends at the end of a buffer
1252
   */
1253
  protected boolean isLastChar() {
1254
    return getTokenEnd() == getBufferEnd();
1✔
1255
  }
1256

1257
  /**
1258
   * @return token type for the back-slash we are standing on. Depends on character ahead: slash for quotes and string for the rest
1259
   */
1260
  protected IElementType getSQBackSlashTokenType() {
1261
    if (mySingleOpenQuoteChar == 0) {
1✔
1262
      return STRING_CONTENT;
1✔
1263
    }
1264
    int tokenEnd = getTokenEnd();
1✔
1265
    int bufferEnd = getBufferEnd();
1✔
1266
    if (bufferEnd <= tokenEnd) {
1✔
1267
      return STRING_CONTENT;
×
1268
    }
1269
    char nextChar = getBuffer().charAt(tokenEnd);
1✔
1270
    return nextChar == '\\' || nextChar == mySingleOpenQuoteChar || nextChar == mySingleCloseQuoteChar ?
1✔
1271
           STRING_SPECIAL_ESCAPE_CHAR : STRING_CONTENT;
1✔
1272
  }
1273
}
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