chevrotain
Version:
Chevrotain is a high performance fault tolerant javascript parsing DSL for building recursive decent parsers
923 lines (822 loc) • 30.3 kB
text/typescript
import {
analyzeTokenTypes,
charCodeToOptimizedIndex,
cloneEmptyGroups,
DEFAULT_MODE,
LineTerminatorOptimizedTester,
performRuntimeChecks,
performWarningRuntimeChecks,
SUPPORT_STICKY,
validatePatterns
} from "./lexer"
import {
cloneArr,
cloneObj,
forEach,
IDENTITY,
isArray,
isEmpty,
isUndefined,
keys,
last,
map,
merge,
NOOP,
PRINT_WARNING,
reduce,
reject,
timer,
toFastProperties
} from "../utils/utils"
import { augmentTokenTypes } from "./tokens"
import {
CustomPatternMatcherFunc,
ILexerConfig,
ILexerDefinitionError,
ILexingError,
IMultiModeLexerDefinition,
IToken,
TokenType
} from "../../api"
import { defaultLexerErrorProvider } from "../scan/lexer_errors_public"
import { clearRegExpParserCache } from "./reg_exp_parser"
export interface ILexingResult {
tokens: IToken[]
groups: { [groupName: string]: IToken[] }
errors: ILexingError[]
}
export enum LexerDefinitionErrorType {
MISSING_PATTERN,
INVALID_PATTERN,
EOI_ANCHOR_FOUND,
UNSUPPORTED_FLAGS_FOUND,
DUPLICATE_PATTERNS_FOUND,
INVALID_GROUP_TYPE_FOUND,
PUSH_MODE_DOES_NOT_EXIST,
MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE,
MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY,
MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST,
LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED,
SOI_ANCHOR_FOUND,
EMPTY_MATCH_PATTERN,
NO_LINE_BREAKS_FLAGS,
UNREACHABLE_PATTERN,
IDENTIFY_TERMINATOR,
CUSTOM_LINE_BREAK
}
export interface IRegExpExec {
exec: CustomPatternMatcherFunc
}
const DEFAULT_LEXER_CONFIG: ILexerConfig = {
deferDefinitionErrorsHandling: false,
positionTracking: "full",
lineTerminatorsPattern: /\n|\r\n?/g,
lineTerminatorCharacters: ["\n", "\r"],
ensureOptimizations: false,
safeMode: false,
errorMessageProvider: defaultLexerErrorProvider,
traceInitPerf: false,
skipValidations: false
}
Object.freeze(DEFAULT_LEXER_CONFIG)
export class Lexer {
public static SKIPPED =
"This marks a skipped Token pattern, this means each token identified by it will" +
"be consumed and then thrown into oblivion, this can be used to for example to completely ignore whitespace."
public static NA = /NOT_APPLICABLE/
public lexerDefinitionErrors: ILexerDefinitionError[] = []
public lexerDefinitionWarning: ILexerDefinitionError[] = []
protected patternIdxToConfig: any = {}
protected charCodeToPatternIdxToConfig: any = {}
protected modes: string[] = []
protected defaultMode: string
protected emptyGroups: { [groupName: string]: IToken } = {}
private config: ILexerConfig = undefined
private trackStartLines: boolean = true
private trackEndLines: boolean = true
private hasCustom: boolean = false
private canModeBeOptimized: any = {}
private traceInitPerf: boolean | number
private traceInitMaxIdent: number
private traceInitIndent: number
constructor(
protected lexerDefinition: TokenType[] | IMultiModeLexerDefinition,
config: ILexerConfig = DEFAULT_LEXER_CONFIG
) {
if (typeof config === "boolean") {
throw Error(
"The second argument to the Lexer constructor is now an ILexerConfig Object.\n" +
"a boolean 2nd argument is no longer supported"
)
}
// todo: defaults func?
this.config = merge(DEFAULT_LEXER_CONFIG, config)
const traceInitVal = this.config.traceInitPerf
if (traceInitVal === true) {
this.traceInitMaxIdent = Infinity
this.traceInitPerf = true
} else if (typeof traceInitVal === "number") {
this.traceInitMaxIdent = traceInitVal
this.traceInitPerf = true
}
this.traceInitIndent = -1
this.TRACE_INIT("Lexer Constructor", () => {
let actualDefinition: IMultiModeLexerDefinition
let hasOnlySingleMode = true
this.TRACE_INIT("Lexer Config handling", () => {
if (
this.config.lineTerminatorsPattern ===
DEFAULT_LEXER_CONFIG.lineTerminatorsPattern
) {
// optimized built-in implementation for the defaults definition of lineTerminators
this.config.lineTerminatorsPattern = LineTerminatorOptimizedTester
} else {
if (
this.config.lineTerminatorCharacters ===
DEFAULT_LEXER_CONFIG.lineTerminatorCharacters
) {
throw Error(
"Error: Missing <lineTerminatorCharacters> property on the Lexer config.\n" +
"\tFor details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS"
)
}
}
if (config.safeMode && config.ensureOptimizations) {
throw Error(
'"safeMode" and "ensureOptimizations" flags are mutually exclusive.'
)
}
this.trackStartLines = /full|onlyStart/i.test(
this.config.positionTracking
)
this.trackEndLines = /full/i.test(this.config.positionTracking)
// Convert SingleModeLexerDefinition into a IMultiModeLexerDefinition.
if (isArray(lexerDefinition)) {
actualDefinition = <any>{ modes: {} }
actualDefinition.modes[DEFAULT_MODE] = cloneArr(
<TokenType[]>lexerDefinition
)
actualDefinition[DEFAULT_MODE] = DEFAULT_MODE
} else {
// no conversion needed, input should already be a IMultiModeLexerDefinition
hasOnlySingleMode = false
actualDefinition = cloneObj(
<IMultiModeLexerDefinition>lexerDefinition
)
}
})
if (this.config.skipValidations === false) {
this.TRACE_INIT("performRuntimeChecks", () => {
this.lexerDefinitionErrors = this.lexerDefinitionErrors.concat(
performRuntimeChecks(
actualDefinition,
this.trackStartLines,
this.config.lineTerminatorCharacters
)
)
})
this.TRACE_INIT("performWarningRuntimeChecks", () => {
this.lexerDefinitionWarning = this.lexerDefinitionWarning.concat(
performWarningRuntimeChecks(
actualDefinition,
this.trackStartLines,
this.config.lineTerminatorCharacters
)
)
})
}
// for extra robustness to avoid throwing an none informative error message
actualDefinition.modes = actualDefinition.modes
? actualDefinition.modes
: {}
// an error of undefined TokenTypes will be detected in "performRuntimeChecks" above.
// this transformation is to increase robustness in the case of partially invalid lexer definition.
forEach(actualDefinition.modes, (currModeValue, currModeName) => {
actualDefinition.modes[currModeName] = reject<TokenType>(
currModeValue,
(currTokType) => isUndefined(currTokType)
)
})
let allModeNames = keys(actualDefinition.modes)
forEach(
actualDefinition.modes,
(currModDef: TokenType[], currModName) => {
this.TRACE_INIT(`Mode: <${currModName}> processing`, () => {
this.modes.push(currModName)
if (this.config.skipValidations === false) {
this.TRACE_INIT(`validatePatterns`, () => {
this.lexerDefinitionErrors = this.lexerDefinitionErrors.concat(
validatePatterns(<TokenType[]>currModDef, allModeNames)
)
})
}
// If definition errors were encountered, the analysis phase may fail unexpectedly/
// Considering a lexer with definition errors may never be used, there is no point
// to performing the analysis anyhow...
if (isEmpty(this.lexerDefinitionErrors)) {
augmentTokenTypes(currModDef)
let currAnalyzeResult
this.TRACE_INIT(`analyzeTokenTypes`, () => {
currAnalyzeResult = analyzeTokenTypes(currModDef, {
lineTerminatorCharacters: this.config
.lineTerminatorCharacters,
positionTracking: config.positionTracking,
ensureOptimizations: config.ensureOptimizations,
safeMode: config.safeMode,
tracer: this.TRACE_INIT.bind(this)
})
})
this.patternIdxToConfig[currModName] =
currAnalyzeResult.patternIdxToConfig
this.charCodeToPatternIdxToConfig[currModName] =
currAnalyzeResult.charCodeToPatternIdxToConfig
this.emptyGroups = merge(
this.emptyGroups,
currAnalyzeResult.emptyGroups
)
this.hasCustom = currAnalyzeResult.hasCustom || this.hasCustom
this.canModeBeOptimized[currModName] =
currAnalyzeResult.canBeOptimized
}
})
}
)
this.defaultMode = actualDefinition.defaultMode
if (
!isEmpty(this.lexerDefinitionErrors) &&
!this.config.deferDefinitionErrorsHandling
) {
let allErrMessages = map(this.lexerDefinitionErrors, (error) => {
return error.message
})
let allErrMessagesString = allErrMessages.join(
"-----------------------\n"
)
throw new Error(
"Errors detected in definition of Lexer:\n" + allErrMessagesString
)
}
// Only print warning if there are no errors, This will avoid pl
forEach(this.lexerDefinitionWarning, (warningDescriptor) => {
PRINT_WARNING(warningDescriptor.message)
})
this.TRACE_INIT("Choosing sub-methods implementations", () => {
// Choose the relevant internal implementations for this specific parser.
// These implementations should be in-lined by the JavaScript engine
// to provide optimal performance in each scenario.
if (SUPPORT_STICKY) {
this.chopInput = <any>IDENTITY
this.match = this.matchWithTest
} else {
this.updateLastIndex = NOOP
this.match = this.matchWithExec
}
if (hasOnlySingleMode) {
this.handleModes = NOOP
}
if (this.trackStartLines === false) {
this.computeNewColumn = IDENTITY
}
if (this.trackEndLines === false) {
this.updateTokenEndLineColumnLocation = NOOP
}
if (/full/i.test(this.config.positionTracking)) {
this.createTokenInstance = this.createFullToken
} else if (/onlyStart/i.test(this.config.positionTracking)) {
this.createTokenInstance = this.createStartOnlyToken
} else if (/onlyOffset/i.test(this.config.positionTracking)) {
this.createTokenInstance = this.createOffsetOnlyToken
} else {
throw Error(
`Invalid <positionTracking> config option: "${this.config.positionTracking}"`
)
}
if (this.hasCustom) {
this.addToken = this.addTokenUsingPush
this.handlePayload = this.handlePayloadWithCustom
} else {
this.addToken = this.addTokenUsingMemberAccess
this.handlePayload = this.handlePayloadNoCustom
}
})
this.TRACE_INIT("Failed Optimization Warnings", () => {
const unOptimizedModes = reduce(
this.canModeBeOptimized,
(cannotBeOptimized, canBeOptimized, modeName) => {
if (canBeOptimized === false) {
cannotBeOptimized.push(modeName)
}
return cannotBeOptimized
},
[]
)
if (config.ensureOptimizations && !isEmpty(unOptimizedModes)) {
throw Error(
`Lexer Modes: < ${unOptimizedModes.join(
", "
)} > cannot be optimized.\n` +
'\t Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode.\n' +
"\t Or inspect the console log for details on how to resolve these issues."
)
}
})
this.TRACE_INIT("clearRegExpParserCache", () => {
clearRegExpParserCache()
})
this.TRACE_INIT("toFastProperties", () => {
toFastProperties(this)
})
})
}
public tokenize(
text: string,
initialMode: string = this.defaultMode
): ILexingResult {
if (!isEmpty(this.lexerDefinitionErrors)) {
let allErrMessages = map(this.lexerDefinitionErrors, (error) => {
return error.message
})
let allErrMessagesString = allErrMessages.join(
"-----------------------\n"
)
throw new Error(
"Unable to Tokenize because Errors detected in definition of Lexer:\n" +
allErrMessagesString
)
}
let lexResult = this.tokenizeInternal(text, initialMode)
return lexResult
}
// There is quite a bit of duplication between this and "tokenizeInternalLazy"
// This is intentional due to performance considerations.
private tokenizeInternal(text: string, initialMode: string): ILexingResult {
let i,
j,
matchAltImage,
longerAltIdx,
matchedImage,
payload,
altPayload,
imageLength,
group,
tokType,
newToken,
errLength,
droppedChar,
msg,
match
let orgText = text
let orgLength = orgText.length
let offset = 0
let matchedTokensIndex = 0
// initializing the tokensArray to the "guessed" size.
// guessing too little will still reduce the number of array re-sizes on pushes.
// guessing too large (Tested by guessing x4 too large) may cost a bit more of memory
// but would still have a faster runtime by avoiding (All but one) array resizing.
let guessedNumberOfTokens = this.hasCustom
? 0 // will break custom token pattern APIs the matchedTokens array will contain undefined elements.
: Math.floor(text.length / 10)
let matchedTokens = new Array(guessedNumberOfTokens)
let errors: ILexingError[] = []
let line = this.trackStartLines ? 1 : undefined
let column = this.trackStartLines ? 1 : undefined
let groups: any = cloneEmptyGroups(this.emptyGroups)
let trackLines = this.trackStartLines
const lineTerminatorPattern = this.config.lineTerminatorsPattern
let currModePatternsLength = 0
let patternIdxToConfig = []
let currCharCodeToPatternIdxToConfig = []
let modeStack = []
const emptyArray = []
Object.freeze(emptyArray)
let getPossiblePatterns = undefined
function getPossiblePatternsSlow() {
return patternIdxToConfig
}
function getPossiblePatternsOptimized(charCode) {
const optimizedCharIdx = charCodeToOptimizedIndex(charCode)
const possiblePatterns =
currCharCodeToPatternIdxToConfig[optimizedCharIdx]
if (possiblePatterns === undefined) {
return emptyArray
} else {
return possiblePatterns
}
}
let pop_mode = (popToken) => {
// TODO: perhaps avoid this error in the edge case there is no more input?
if (
modeStack.length === 1 &&
// if we have both a POP_MODE and a PUSH_MODE this is in-fact a "transition"
// So no error should occur.
popToken.tokenType.PUSH_MODE === undefined
) {
// if we try to pop the last mode there lexer will no longer have ANY mode.
// thus the pop is ignored, an error will be created and the lexer will continue parsing in the previous mode.
let msg = this.config.errorMessageProvider.buildUnableToPopLexerModeMessage(
popToken
)
errors.push({
offset: popToken.startOffset,
line:
popToken.startLine !== undefined ? popToken.startLine : undefined,
column:
popToken.startColumn !== undefined
? popToken.startColumn
: undefined,
length: popToken.image.length,
message: msg
})
} else {
modeStack.pop()
let newMode = last(modeStack)
patternIdxToConfig = this.patternIdxToConfig[newMode]
currCharCodeToPatternIdxToConfig = this.charCodeToPatternIdxToConfig[
newMode
]
currModePatternsLength = patternIdxToConfig.length
const modeCanBeOptimized =
this.canModeBeOptimized[newMode] && this.config.safeMode === false
if (currCharCodeToPatternIdxToConfig && modeCanBeOptimized) {
getPossiblePatterns = getPossiblePatternsOptimized
} else {
getPossiblePatterns = getPossiblePatternsSlow
}
}
}
function push_mode(newMode) {
modeStack.push(newMode)
currCharCodeToPatternIdxToConfig = this.charCodeToPatternIdxToConfig[
newMode
]
patternIdxToConfig = this.patternIdxToConfig[newMode]
currModePatternsLength = patternIdxToConfig.length
currModePatternsLength = patternIdxToConfig.length
const modeCanBeOptimized =
this.canModeBeOptimized[newMode] && this.config.safeMode === false
if (currCharCodeToPatternIdxToConfig && modeCanBeOptimized) {
getPossiblePatterns = getPossiblePatternsOptimized
} else {
getPossiblePatterns = getPossiblePatternsSlow
}
}
// this pattern seems to avoid a V8 de-optimization, although that de-optimization does not
// seem to matter performance wise.
push_mode.call(this, initialMode)
let currConfig
while (offset < orgLength) {
matchedImage = null
let nextCharCode = orgText.charCodeAt(offset)
const chosenPatternIdxToConfig = getPossiblePatterns(nextCharCode)
let chosenPatternsLength = chosenPatternIdxToConfig.length
for (i = 0; i < chosenPatternsLength; i++) {
currConfig = chosenPatternIdxToConfig[i]
let currPattern = currConfig.pattern
payload = null
// manually in-lined because > 600 chars won't be in-lined in V8
let singleCharCode = currConfig.short
if (singleCharCode !== false) {
if (nextCharCode === singleCharCode) {
// single character string
matchedImage = currPattern
}
} else if (currConfig.isCustom === true) {
match = currPattern.exec(orgText, offset, matchedTokens, groups)
if (match !== null) {
matchedImage = match[0]
if (match.payload !== undefined) {
payload = match.payload
}
} else {
matchedImage = null
}
} else {
this.updateLastIndex(currPattern, offset)
matchedImage = this.match(currPattern, text, offset)
}
if (matchedImage !== null) {
// even though this pattern matched we must try a another longer alternative.
// this can be used to prioritize keywords over identifiers
longerAltIdx = currConfig.longerAlt
if (longerAltIdx !== undefined) {
// TODO: micro optimize, avoid extra prop access
// by saving/linking longerAlt on the original config?
let longerAltConfig = patternIdxToConfig[longerAltIdx]
let longerAltPattern = longerAltConfig.pattern
altPayload = null
// single Char can never be a longer alt so no need to test it.
// manually in-lined because > 600 chars won't be in-lined in V8
if (longerAltConfig.isCustom === true) {
match = longerAltPattern.exec(
orgText,
offset,
matchedTokens,
groups
)
if (match !== null) {
matchAltImage = match[0]
if (match.payload !== undefined) {
altPayload = match.payload
}
} else {
matchAltImage = null
}
} else {
this.updateLastIndex(longerAltPattern, offset)
matchAltImage = this.match(longerAltPattern, text, offset)
}
if (matchAltImage && matchAltImage.length > matchedImage.length) {
matchedImage = matchAltImage
payload = altPayload
currConfig = longerAltConfig
}
}
break
}
}
// successful match
if (matchedImage !== null) {
imageLength = matchedImage.length
group = currConfig.group
if (group !== undefined) {
tokType = currConfig.tokenTypeIdx
// TODO: "offset + imageLength" and the new column may be computed twice in case of "full" location information inside
// createFullToken method
newToken = this.createTokenInstance(
matchedImage,
offset,
tokType,
currConfig.tokenType,
line,
column,
imageLength
)
this.handlePayload(newToken, payload)
// TODO: optimize NOOP in case there are no special groups?
if (group === false) {
matchedTokensIndex = this.addToken(
matchedTokens,
matchedTokensIndex,
newToken
)
} else {
groups[group].push(newToken)
}
}
text = this.chopInput(text, imageLength)
offset = offset + imageLength
// TODO: with newlines the column may be assigned twice
column = this.computeNewColumn(column, imageLength)
if (trackLines === true && currConfig.canLineTerminator === true) {
let numOfLTsInMatch = 0
let foundTerminator
let lastLTEndOffset
lineTerminatorPattern.lastIndex = 0
do {
foundTerminator = lineTerminatorPattern.test(matchedImage)
if (foundTerminator === true) {
lastLTEndOffset = lineTerminatorPattern.lastIndex - 1
numOfLTsInMatch++
}
} while (foundTerminator === true)
if (numOfLTsInMatch !== 0) {
line = line + numOfLTsInMatch
column = imageLength - lastLTEndOffset
this.updateTokenEndLineColumnLocation(
newToken,
group,
lastLTEndOffset,
numOfLTsInMatch,
line,
column,
imageLength
)
}
}
// will be NOOP if no modes present
this.handleModes(currConfig, pop_mode, push_mode, newToken)
} else {
// error recovery, drop characters until we identify a valid token's start point
let errorStartOffset = offset
let errorLine = line
let errorColumn = column
let foundResyncPoint = false
while (!foundResyncPoint && offset < orgLength) {
// drop chars until we succeed in matching something
droppedChar = orgText.charCodeAt(offset)
// Identity Func (when sticky flag is enabled)
text = this.chopInput(text, 1)
offset++
for (j = 0; j < currModePatternsLength; j++) {
let currConfig = patternIdxToConfig[j]
let currPattern = currConfig.pattern
// manually in-lined because > 600 chars won't be in-lined in V8
let singleCharCode = currConfig.short
if (singleCharCode !== false) {
if (orgText.charCodeAt(offset) === singleCharCode) {
// single character string
foundResyncPoint = true
}
} else if (currConfig.isCustom === true) {
foundResyncPoint =
currPattern.exec(orgText, offset, matchedTokens, groups) !==
null
} else {
this.updateLastIndex(currPattern, offset)
foundResyncPoint = currPattern.exec(text) !== null
}
if (foundResyncPoint === true) {
break
}
}
}
errLength = offset - errorStartOffset
// at this point we either re-synced or reached the end of the input text
msg = this.config.errorMessageProvider.buildUnexpectedCharactersMessage(
orgText,
errorStartOffset,
errLength,
errorLine,
errorColumn
)
errors.push({
offset: errorStartOffset,
line: errorLine,
column: errorColumn,
length: errLength,
message: msg
})
}
}
// if we do have custom patterns which push directly into the
// TODO: custom tokens should not push directly??
if (!this.hasCustom) {
// if we guessed a too large size for the tokens array this will shrink it to the right size.
matchedTokens.length = matchedTokensIndex
}
return {
tokens: matchedTokens,
groups: groups,
errors: errors
}
}
private handleModes(config, pop_mode, push_mode, newToken) {
if (config.pop === true) {
// need to save the PUSH_MODE property as if the mode is popped
// patternIdxToPopMode is updated to reflect the new mode after popping the stack
let pushMode = config.push
pop_mode(newToken)
if (pushMode !== undefined) {
push_mode.call(this, pushMode)
}
} else if (config.push !== undefined) {
push_mode.call(this, config.push)
}
}
private chopInput(text, length): string {
return text.substring(length)
}
private updateLastIndex(regExp, newLastIndex): void {
regExp.lastIndex = newLastIndex
}
// TODO: decrease this under 600 characters? inspect stripping comments option in TSC compiler
private updateTokenEndLineColumnLocation(
newToken,
group,
lastLTIdx,
numOfLTsInMatch,
line,
column,
imageLength
): void {
let lastCharIsLT, fixForEndingInLT
if (group !== undefined) {
// a none skipped multi line Token, need to update endLine/endColumn
lastCharIsLT = lastLTIdx === imageLength - 1
fixForEndingInLT = lastCharIsLT ? -1 : 0
if (!(numOfLTsInMatch === 1 && lastCharIsLT === true)) {
// if a token ends in a LT that last LT only affects the line numbering of following Tokens
newToken.endLine = line + fixForEndingInLT
// the last LT in a token does not affect the endColumn either as the [columnStart ... columnEnd)
// inclusive to exclusive range.
newToken.endColumn = column - 1 + -fixForEndingInLT
}
// else single LT in the last character of a token, no need to modify the endLine/EndColumn
}
}
private computeNewColumn(oldColumn, imageLength) {
return oldColumn + imageLength
}
// Place holder, will be replaced by the correct variant according to the locationTracking option at runtime.
/* istanbul ignore next - place holder */
private createTokenInstance(...args: any[]): IToken {
return null
}
private createOffsetOnlyToken(image, startOffset, tokenTypeIdx, tokenType) {
return {
image,
startOffset,
tokenTypeIdx,
tokenType
}
}
private createStartOnlyToken(
image,
startOffset,
tokenTypeIdx,
tokenType,
startLine,
startColumn
) {
return {
image,
startOffset,
startLine,
startColumn,
tokenTypeIdx,
tokenType
}
}
private createFullToken(
image,
startOffset,
tokenTypeIdx,
tokenType,
startLine,
startColumn,
imageLength
) {
return {
image,
startOffset,
endOffset: startOffset + imageLength - 1,
startLine,
endLine: startLine,
startColumn,
endColumn: startColumn + imageLength - 1,
tokenTypeIdx,
tokenType
}
}
// Place holder, will be replaced by the correct variant according to the locationTracking option at runtime.
/* istanbul ignore next - place holder */
private addToken(tokenVector, index, tokenToAdd): number {
return 666
}
private addTokenUsingPush(tokenVector, index, tokenToAdd): number {
tokenVector.push(tokenToAdd)
return index
}
private addTokenUsingMemberAccess(tokenVector, index, tokenToAdd): number {
tokenVector[index] = tokenToAdd
index++
return index
}
// Place holder, will be replaced by the correct variant according to the hasCustom flag option at runtime.
/* istanbul ignore next - place holder */
private handlePayload(token: IToken, payload: any): void {}
private handlePayloadNoCustom(token: IToken, payload: any): void {}
private handlePayloadWithCustom(token: IToken, payload: any): void {
if (payload !== null) {
token.payload = payload
}
}
/* istanbul ignore next - place holder to be replaced with chosen alternative at runtime */
private match(pattern: RegExp, text: string, offset?: number): string {
return null
}
private matchWithTest(pattern: RegExp, text: string, offset: number): string {
let found = pattern.test(text)
if (found === true) {
return text.substring(offset, pattern.lastIndex)
}
return null
}
private matchWithExec(pattern, text): string {
let regExpArray = pattern.exec(text)
return regExpArray !== null ? regExpArray[0] : regExpArray
}
// Duplicated from the parser's perf trace trait to allow future extraction
// of the lexer to a separate package.
TRACE_INIT<T>(phaseDesc: string, phaseImpl: () => T): T {
// No need to optimize this using NOOP pattern because
// It is not called in a hot spot...
if (this.traceInitPerf === true) {
this.traceInitIndent++
const indent = new Array(this.traceInitIndent + 1).join("\t")
if (this.traceInitIndent < this.traceInitMaxIdent) {
console.log(`${indent}--> <${phaseDesc}>`)
}
const { time, value } = timer(phaseImpl)
/* istanbul ignore next - Difficult to reproduce specific performance behavior (>10ms) in tests */
const traceMethod = time > 10 ? console.warn : console.log
if (this.traceInitIndent < this.traceInitMaxIdent) {
traceMethod(`${indent}<-- <${phaseDesc}> time: ${time}ms`)
}
this.traceInitIndent--
return value
} else {
return phaseImpl()
}
}
}