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

Camelcade / Perl5-IDEA / #525521586

19 Jun 2025 10:27AM UTC coverage: 82.376% (+0.03%) from 82.349%
#525521586

push

github

hurricup
Migrated to NioFiles.deleteRecursively

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

54 existing lines in 7 files now uncovered.

30872 of 37477 relevant lines covered (82.38%)

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
    //noinspection deprecation
84
    PerlParserExtension.EP_NAME.addChangeListener(PerlBaseLexer::refreshExtensions, PerlPluginUtil.getUnloadAwareDisposable());
1✔
85
    refreshExtensions();
1✔
86
    return true;
1✔
87
  });
88

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

475

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

503
  /**
504
   * Fast POD block capture method. Invoked after opening line was captured. No line beginning check was preformed.
505
   *
506
   * @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.
507
   */
508
  protected final IElementType capturePod(boolean isEndless) {
509
    var tokenStart = getTokenStart();
1✔
510
    var buffer = getBuffer();
1✔
511
    if (tokenStart != 0 && buffer.charAt(tokenStart - 1) != '\n') {
1✔
512
      yypushback(yylength() - 1);
1✔
513
      yybegin(YYINITIAL);
1✔
514
      return OPERATOR_ASSIGN;
1✔
515
    }
516

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

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

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

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

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

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

612
    yybegin(AFTER_IDENTIFIER);
1✔
613

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

620
    return tokenType;
1✔
621
  }
622

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

764

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

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

777
    return getPreParsedToken();
1✔
778
  }
779

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

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

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

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

818
    return currentOffset;
1✔
819
  }
820

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

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

858
    return currentOffset;
1✔
859
  }
860

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

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

869
    int delimiterLevel = 0;
1✔
870

871
    int currentOffset = startOffset;
1✔
872

873
    while (currentOffset < bufferEnd) {
1✔
874

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

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

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

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

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

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

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

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

924
    currentOffset = firstBlockEndOffset;
1✔
925

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

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

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

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

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

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

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

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

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

1021
    return getPreParsedToken();
1✔
1022
  }
1023

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

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

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

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

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

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

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

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

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

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

1116
    var bufferEnd = getBufferEnd();
1✔
1117

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

1126
    int markerEndOffset = markerStartOffset + 1;
1✔
1127

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

1134
    return getPreParsedToken();
1✔
1135
  }
1136

1137

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

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

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

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

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

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

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

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

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

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

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

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