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

Camelcade / Perl5-IDEA / #525521660

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

push

github

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

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

14751 of 22639 branches covered (65.16%)

Branch coverage included in aggregate %.

31091 of 37767 relevant lines covered (82.32%)

0.82 hits per line

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

94.42
/plugin/common/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.psi.PerlString;
33
import com.perl5.lang.perl.psi.references.PerlImplicitDeclarationsService;
34
import com.perl5.lang.perl.util.PerlPackageUtilCore;
35
import com.perl5.lang.perl.util.PerlPluginUtil;
36
import org.jetbrains.annotations.NotNull;
37
import org.jetbrains.annotations.Nullable;
38

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

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

46

47
public abstract class PerlBaseLexer extends PerlProtoLexer {
48
  private static final Logger LOG = Logger.getInstance(PerlBaseLexer.class);
1✔
49

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

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

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

77
  protected static final String SUB_SIGNATURE = "Sub.Signature";
78

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

86
  private static void refreshExtensions() {
87
    PerlParserImpl.restoreDefaultExtendsSet();
1✔
88

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

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

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

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

133
  public PerlBaseLexer() {
1✔
134
    ourListenersInitializer.get();
1✔
135
  }
1✔
136

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

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

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

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

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

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

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

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

212
  protected IElementType startBracedBlock() {
213
    return startBracedBlockWithState(YYINITIAL);
1✔
214
  }
215

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

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

233
  protected IElementType getLeftBrace() {
234
    return getLeftBrace(YYINITIAL);
1✔
235
  }
236

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

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

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

263
  protected IElementType startBracketedBlock() {
264
    return startBracketedBlockWithState(YYINITIAL);
1✔
265
  }
266

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

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

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

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

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

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

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

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

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

360
  protected @NotNull IElementType getBracedVariableNameToken() {
361
    yybegin(YYINITIAL);
1✔
362
    return getVariableNameTokenBySigil();
1!
363
  }
364

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

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

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

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

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

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

403
    myImplicitSubsService = myProject == null ? null : PerlImplicitDeclarationsService.getInstance(myProject);
1✔
404

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

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

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

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

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

472

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

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

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

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

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

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

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

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

609
    yybegin(AFTER_IDENTIFIER);
1✔
610

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

617
    return tokenType;
1✔
618
  }
619

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

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

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

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

654
    char openQuote = buffer.charAt(currentPosition);
1✔
655

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

659
    int contentStart = currentPosition;
1✔
660
    boolean hasEscape = false;
1✔
661
    boolean hasSigil = false;
1✔
662

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

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

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

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

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

689
      currentPosition++;
1✔
690
    }
1✔
691

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

704
      pushPreparsedToken(contentStart, currentPosition, contentTokenType);
1✔
705
    }
706

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

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

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

733
    currentOffset = parseTrBlockContent(currentOffset, openQuote, closeQuote);
1✔
734

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

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

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

753
      currentOffset = parseTrBlockContent(currentOffset, openQuote, closeQuote);
1✔
754
    }
755

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

761

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

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

774
    return getPreParsedToken();
1✔
775
  }
776

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

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

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

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

815
    return currentOffset;
1✔
816
  }
817

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

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

855
    return currentOffset;
1✔
856
  }
857

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

863
    boolean isEscaped = false;
1✔
864
    boolean isQuotesDiffers = closingChar != openingChar;
1✔
865

866
    int delimiterLevel = 0;
1✔
867

868
    int currentOffset = startOffset;
1✔
869

870
    while (currentOffset < bufferEnd) {
1✔
871

872
      char currentChar = buffer.charAt(currentOffset);
1✔
873

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

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

887
      isEscaped = !isEscaped && closingChar != '\\' && currentChar == '\\';
1!
888

889
      currentOffset++;
1✔
890
    }
1✔
891
    return currentOffset;
1✔
892
  }
893

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

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

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

921
    currentOffset = firstBlockEndOffset;
1✔
922

923
    // find block 2
924
    CustomToken secondBlockOpeningToken = null;
1✔
925
    CustomToken secondBlockToken = null;
1✔
926

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

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

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

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

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

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

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

1014
      pushPreparsedToken(currentOffset, modifierEndOffset, REGEX_MODIFIER);
1✔
1015
      currentOffset = modifierEndOffset;
1✔
1016
    }
1✔
1017

1018
    return getPreParsedToken();
1✔
1019
  }
1020

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

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

1045
    var closeMarker = currentHeredoc.getMarker();
1✔
1046

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

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

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

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

1090
    yybegin(YYINITIAL);
1✔
1091
    setTokenEnd(bufferEnd);
1✔
1092
    return currentHeredoc.getTargetElement();
1!
1093
  }
1094

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

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

1113
    var bufferEnd = getBufferEnd();
1✔
1114

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

1123
    int markerEndOffset = markerStartOffset + 1;
1✔
1124

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

1131
    return getPreParsedToken();
1!
1132
  }
1133

1134

1135
  protected IElementType registerPackage(IElementType tokenType) {
1136
    myLocalPackages.add(PerlPackageUtilCore.getCanonicalNamespaceName(yytext().toString()));
1✔
1137
    return tokenType;
1✔
1138
  }
1139

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

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

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

1193
  public boolean isHeredocLike() {
1194
    return myIsHeredocLike;
1✔
1195
  }
1196

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

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

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

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

1244
  public @NotNull IElementType getLPCodeBlockElementType() {
1245
    return hasTryCatch() ? LP_CODE_BLOCK_WITH_TRYCATCH : LP_CODE_BLOCK;
1!
1246
  }
1247

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

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