From 7b43ef69cfe90eec3333e572a3ad0ec623b73058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:00:27 +0000 Subject: [PATCH 1/5] Initial plan From 4a067a75de05d7a71753914078ff148ca14e89d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:05:48 +0000 Subject: [PATCH 2/5] Initial exploration of codebase for code review Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- yarn.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index cfec4ad..8849065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,10 +47,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@esbuild/win32-x64@0.25.10": +"@esbuild/linux-x64@0.25.10": version "0.25.10" - resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz" - integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw== + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz" + integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA== "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0", "@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.0" @@ -303,15 +303,15 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-win32-x64-gnu@4.52.0": +"@rollup/rollup-linux-x64-gnu@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz" - integrity sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw== + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz" + integrity sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA== -"@rollup/rollup-win32-x64-msvc@4.52.0": +"@rollup/rollup-linux-x64-musl@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz" - integrity sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ== + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz" + integrity sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -515,10 +515,10 @@ "@typescript-eslint/types" "8.44.0" eslint-visitor-keys "^4.2.1" -"@typescript/native-preview-win32-x64@7.0.0-dev.20250920.1": +"@typescript/native-preview-linux-x64@7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1" - resolved "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250920.1.tgz" - integrity sha512-arxNEOP4LMr+rxCxbitG9ejEkmvvj7pkjMFTAFHfWJC0mKuT+1cQu79iZBhY16oHB4HLCziJRBRdMQhIB2Wf+w== + resolved "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250920.1.tgz" + integrity sha512-6slYt8MKVAOZDbs2GXznPJ7KVmMLssqvmTJOmmNNyNJt/0MBJqwgL1b/TlxS9UgUTuBsNyKJbLtJgjvBVixIAA== "@typescript/native-preview@^7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1" From 216f82855e8170a2a4aa4de61f180c6da5d7dbdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:10:30 +0000 Subject: [PATCH 3/5] Code review improvements: centralize types, add caching, improve documentation Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- src/core/contains.ts | 18 +++++----- src/core/evaluate.ts | 27 +++++++------- src/core/simplify.ts | 4 +-- src/language-service/language-service.ts | 44 ++++++++++++++++++++--- src/parsing/instruction.ts | 46 ++++++++++++++++++++++-- src/parsing/parser.ts | 28 +++++---------- src/parsing/token-stream.ts | 13 ++++--- src/parsing/token.ts | 39 +++++++++++++++++--- 8 files changed, 159 insertions(+), 60 deletions(-) diff --git a/src/core/contains.ts b/src/core/contains.ts index 11ed6cc..6d4487f 100644 --- a/src/core/contains.ts +++ b/src/core/contains.ts @@ -1,9 +1,11 @@ -export default function contains(array: T[], obj: T): boolean { - for (let i = 0; i < array.length; i++) { - if (array[i] === obj) { - return true; - } - } - - return false; +/** + * Checks if an array contains a specific value + * Uses strict equality (===) for comparison + * + * @param array - The array to search in + * @param obj - The value to search for + * @returns true if the array contains the value, false otherwise + */ +export default function contains(array: readonly T[], obj: T): boolean { + return array.includes(obj); } diff --git a/src/core/evaluate.ts b/src/core/evaluate.ts index 78585f9..fb47de4 100644 --- a/src/core/evaluate.ts +++ b/src/core/evaluate.ts @@ -1,7 +1,14 @@ +/** + * Expression evaluation module + * + * This module contains the core evaluation logic for executing parsed expressions. + * It uses a stack-based interpreter to evaluate instruction sequences produced by the parser. + */ + import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, IWHENMATCH, ICASEELSE, ICASECOND, IWHENCOND, IOBJECT, IPROPERTY, IOBJECTEND } from '../parsing/instruction.js'; import type { Instruction } from '../parsing/instruction.js'; import type { Expression } from './expression.js'; -import type { Value, Values } from '../types/values.js'; +import type { Value, Values, VariableResolveResult, VariableAlias, VariableValue } from '../types/values.js'; import { VariableError } from '../types/errors.js'; import { ExpressionValidator } from '../validation/expression-validator.js'; @@ -10,23 +17,19 @@ import { ExpressionValidator } from '../validation/expression-validator.js'; // cSpell:words IOBJECT IOBJECTEND // cSpell:words nstack -// Resolver result types (matching parser definitions) -interface VariableAlias { - alias: string; -} - -interface VariableValue { - value: Value; -} - -type VariableResolveResult = VariableAlias | VariableValue | Value | undefined; - +/** + * Wrapper for lazy expression evaluation + * Used for short-circuit evaluation of logical operators and conditionals + */ interface ExpressionEvaluator { type: typeof IEXPREVAL; value: (scope: Values) => Value | Promise; } +/** Type alias for evaluation context values */ type EvaluationValues = Values; + +/** Type alias for the evaluation stack (stores intermediate results) */ type EvaluationStack = any[]; /** diff --git a/src/core/simplify.ts b/src/core/simplify.ts index ae2e386..0b5f2f9 100644 --- a/src/core/simplify.ts +++ b/src/core/simplify.ts @@ -1,7 +1,5 @@ import { Instruction, INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IEXPR, IMEMBER, IARRAY } from '../parsing/instruction.js'; - -// Type for operator functions -type OperatorFunction = (...args: any[]) => any; +import type { OperatorFunction } from '../types/parser.js'; export default function simplify( tokens: Instruction[], diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index 607817e..dad68b2 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -46,7 +46,21 @@ export function createLanguageService(options: LanguageServiceOptions | undefine ...DEFAULT_CONSTANT_DOCS } as Record; + // Cache function details and names for performance + // These are computed once and reused across all calls + let cachedFunctions: FunctionDetails[] | null = null; + let cachedFunctionNames: Set | null = null; + let cachedConstants: string[] | null = null; + + /** + * Returns all available functions with their details + * Results are cached for performance + */ function allFunctions(): FunctionDetails[] { + if (cachedFunctions !== null) { + return cachedFunctions; + } + // Parser exposes built-in functions on parser.functions const definedFunctions = parser.functions ? Object.keys(parser.functions) : []; // Unary operators can also be used like functions with parens: sin(x), abs(x), ... @@ -54,11 +68,32 @@ export function createLanguageService(options: LanguageServiceOptions | undefine // Merge, prefer functions map descriptions where available const rawFunctions = Array.from(new Set([...definedFunctions, ...unary])); - return rawFunctions.map(name => new FunctionDetails(parser, name)); + cachedFunctions = rawFunctions.map(name => new FunctionDetails(parser, name)); + cachedFunctionNames = new Set(rawFunctions); + return cachedFunctions; + } + + /** + * Returns a set of function names for fast lookup + */ + function functionNamesSet(): Set { + if (cachedFunctionNames !== null) { + return cachedFunctionNames; + } + allFunctions(); // This populates cachedFunctionNames + return cachedFunctionNames!; } + /** + * Returns all available constants + * Results are cached for performance + */ function allConstants(): string[] { - return parser.consts ? Object.keys(parser.consts) : []; + if (cachedConstants !== null) { + return cachedConstants; + } + cachedConstants = parser.consts ? Object.keys(parser.consts) : []; + return cachedConstants; } function tokenKindToHighlight(t: Token): HighlightToken['type'] { @@ -79,9 +114,8 @@ export function createLanguageService(options: LanguageServiceOptions | undefine return 'punctuation'; case TNAME: default: { - // If not matches, check if it's a function or an identifier - const functions = allFunctions(); - if (t.type === TNAME && functions.find((f: FunctionDetails) => f.name == String(t.value))) { + // Use cached set for fast function name lookup + if (t.type === TNAME && functionNamesSet().has(String(t.value))) { return 'function'; } diff --git a/src/parsing/instruction.ts b/src/parsing/instruction.ts index 8a54a72..7b009a4 100644 --- a/src/parsing/instruction.ts +++ b/src/parsing/instruction.ts @@ -2,31 +2,69 @@ // cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY // cSpell:words IOBJECT IOBJECTEND -// Instruction type constants +/** + * Instruction types for the expression evaluator's bytecode-style representation. + * + * The parser converts expressions into a sequence of instructions that are + * executed by the evaluator in a stack-based manner (reverse polish notation). + * + * Instruction type naming convention: + * - I = Instruction prefix + * - NUMBER = numeric literal + * - OP1/OP2/OP3 = unary/binary/ternary operators + * - VAR = variable reference + * - FUNCALL = function call + * - etc. + */ + +/** Numeric literal instruction */ export const INUMBER = 'INUMBER' as const; +/** Unary operator instruction (e.g., negation, factorial) */ export const IOP1 = 'IOP1' as const; +/** Binary operator instruction (e.g., +, -, *, /) */ export const IOP2 = 'IOP2' as const; +/** Ternary operator instruction (e.g., conditional ?) */ export const IOP3 = 'IOP3' as const; +/** Variable reference instruction */ export const IVAR = 'IVAR' as const; +/** Variable name instruction (used in assignments) */ export const IVARNAME = 'IVARNAME' as const; +/** Function call instruction */ export const IFUNCALL = 'IFUNCALL' as const; +/** Function definition instruction */ export const IFUNDEF = 'IFUNDEF' as const; +/** Expression instruction (for lazy evaluation) */ export const IEXPR = 'IEXPR' as const; +/** Expression evaluator instruction (compiled expression) */ export const IEXPREVAL = 'IEXPREVAL' as const; +/** Member access instruction (e.g., obj.property) */ export const IMEMBER = 'IMEMBER' as const; +/** End of statement instruction (for multi-statement expressions) */ export const IENDSTATEMENT = 'IENDSTATEMENT' as const; +/** Array literal instruction */ export const IARRAY = 'IARRAY' as const; +/** Undefined value instruction */ export const IUNDEFINED = 'IUNDEFINED' as const; +/** CASE condition instruction (for CASE WHEN without input) */ export const ICASECOND = 'ICASECOND' as const; +/** CASE match instruction (for CASE $input WHEN) */ export const ICASEMATCH = 'ICASEMATCH' as const; +/** WHEN condition instruction */ export const IWHENCOND = 'IWHENCOND' as const; +/** WHEN match instruction */ export const IWHENMATCH = 'IWHENMATCH' as const; +/** CASE ELSE instruction */ export const ICASEELSE = 'ICASEELSE' as const; +/** Object property instruction */ export const IPROPERTY = 'IPROPERTY' as const; +/** Object start instruction */ export const IOBJECT = 'IOBJECT' as const; +/** Object end instruction */ export const IOBJECTEND = 'IOBJECTEND' as const; -// Union type for all instruction types +/** + * Union type for all instruction types + */ export type InstructionType = | typeof INUMBER | typeof IOP1 @@ -51,7 +89,9 @@ export type InstructionType = | typeof IOBJECT | typeof IOBJECTEND; -// Discriminated union types for better type safety +/** + * Discriminated union types for better type safety + */ export interface NumberInstruction { type: typeof INUMBER; value: number; diff --git a/src/parsing/parser.ts b/src/parsing/parser.ts index e7cc4ec..476d5cb 100644 --- a/src/parsing/parser.ts +++ b/src/parsing/parser.ts @@ -3,8 +3,9 @@ import { TEOF } from './token.js'; import { TokenStream } from './token-stream.js'; import { ParserState } from './parser-state.js'; import { Expression } from '../core/expression.js'; -import type { Value } from '../types/values.js'; +import type { Value, VariableResolveResult, Values } from '../types/values.js'; import type { Instruction } from './instruction.js'; +import type { OperatorFunction } from '../types/parser.js'; import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, stringJoin, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight } from '../functions/index.js'; import { add, @@ -62,32 +63,19 @@ import { log2 } from '../operators/unary/index.js'; -// Type for operator functions - they accept arrays of values and return a value -type OperatorFunction = (...args: any[]) => any; - -// Parser options interface +/** + * Parser options configuration + */ interface ParserOptions { allowMemberAccess?: boolean; operators?: Record; } -// Variable resolution result types -interface VariableAlias { - alias: string; -} - -interface VariableValue { - value: Value; -} - -type VariableResolveResult = VariableAlias | VariableValue | Value | undefined; - -// Variable resolver function type +/** + * Variable resolver function type for custom variable resolution + */ type VariableResolver = (token: string) => VariableResolveResult; -// Values object for evaluation -type Values = Record; - export class Parser { public options: ParserOptions; public keywords: string[]; diff --git a/src/parsing/token-stream.ts b/src/parsing/token-stream.ts index 0382caf..86e4b6c 100644 --- a/src/parsing/token-stream.ts +++ b/src/parsing/token-stream.ts @@ -2,11 +2,12 @@ import { Token, TEOF, TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TKEYWORD, TBRACE, TokenType, TokenValue } from './token.js'; import { ParseError } from '../types/errors.js'; +import type { OperatorFunction } from '../types/parser.js'; -// Type for operator functions - they accept arrays of values and return a value -type OperatorFunction = (...args: any[]) => any; - -// Basic parser interface - will define more completely when converting parser.js +/** + * Parser interface defining the required properties for tokenization + * This interface represents the subset of Parser functionality needed by TokenStream + */ interface ParserLike { keywords: string[]; unaryOps: Record; @@ -21,7 +22,9 @@ interface ParserLike { isOperatorEnabled(op: string): boolean; } -// Coordinates interface for error reporting +/** + * Position coordinates for error reporting + */ interface Coordinates { line: number; column: number; diff --git a/src/parsing/token.ts b/src/parsing/token.ts index ea9641f..6bca522 100644 --- a/src/parsing/token.ts +++ b/src/parsing/token.ts @@ -1,19 +1,45 @@ // cSpell:words TEOF TNUMBER TSTRING TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE -// Token type constants +/** + * Token types for the expression lexer/tokenizer. + * + * The tokenizer converts expression strings into a sequence of tokens + * that are then processed by the parser to create instructions. + * + * Token type naming convention: + * - T = Token prefix + * - EOF = End of file/expression + * - OP = Operator + * - NUMBER = Numeric literal + * - etc. + */ + +/** End of file/expression token */ export const TEOF = 'TEOF' as const; +/** Operator token (+, -, *, /, etc.) */ export const TOP = 'TOP' as const; +/** Numeric literal token */ export const TNUMBER = 'TNUMBER' as const; +/** String literal token */ export const TSTRING = 'TSTRING' as const; +/** Parenthesis token ( or ) */ export const TPAREN = 'TPAREN' as const; +/** Bracket token [ or ] */ export const TBRACKET = 'TBRACKET' as const; +/** Comma separator token */ export const TCOMMA = 'TCOMMA' as const; +/** Name/identifier token (variable, function name) */ export const TNAME = 'TNAME' as const; +/** Semicolon statement separator token */ export const TSEMICOLON = 'TSEMICOLON' as const; +/** Keyword token (case, when, then, else, end) */ export const TKEYWORD = 'TKEYWORD' as const; +/** Brace token { or } */ export const TBRACE = 'TBRACE' as const; -// Union type for all token types +/** + * Union type for all token types + */ export type TokenType = | typeof TEOF | typeof TOP @@ -27,13 +53,18 @@ export type TokenType = | typeof TKEYWORD | typeof TBRACE; -// Token value can be various types depending on the token type +/** + * Token value can be various types depending on the token type + */ export type TokenValue = string | number | boolean | undefined; -// Token class with TypeScript typing +/** + * Token class representing a single lexical unit in an expression + */ export class Token { public readonly type: TokenType; public readonly value: TokenValue; + /** Position index in the source expression string */ public readonly index: number; constructor(type: TokenType, value: TokenValue, index: number) { From 083957173b5e06b52b62671d39716a3500bf72cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:14:43 +0000 Subject: [PATCH 4/5] Address code review feedback and restore yarn.lock Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- src/language-service/language-service.ts | 17 +++++++++++------ yarn.lock | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index dad68b2..050852a 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -46,15 +46,16 @@ export function createLanguageService(options: LanguageServiceOptions | undefine ...DEFAULT_CONSTANT_DOCS } as Record; - // Cache function details and names for performance - // These are computed once and reused across all calls + // Instance-level cache for function details and names + // Each language service instance maintains its own cache, making this thread-safe + // as concurrent uses will operate on separate instances let cachedFunctions: FunctionDetails[] | null = null; let cachedFunctionNames: Set | null = null; let cachedConstants: string[] | null = null; /** * Returns all available functions with their details - * Results are cached for performance + * Results are cached for performance within this instance */ function allFunctions(): FunctionDetails[] { if (cachedFunctions !== null) { @@ -75,18 +76,22 @@ export function createLanguageService(options: LanguageServiceOptions | undefine /** * Returns a set of function names for fast lookup + * This ensures the cache is populated before returning */ function functionNamesSet(): Set { if (cachedFunctionNames !== null) { return cachedFunctionNames; } - allFunctions(); // This populates cachedFunctionNames - return cachedFunctionNames!; + // Calling allFunctions() ensures cachedFunctionNames is populated + allFunctions(); + // After allFunctions(), cachedFunctionNames is guaranteed to be non-null + // We return a fallback empty set only as a defensive measure + return cachedFunctionNames ?? new Set(); } /** * Returns all available constants - * Results are cached for performance + * Results are cached for performance within this instance */ function allConstants(): string[] { if (cachedConstants !== null) { diff --git a/yarn.lock b/yarn.lock index 8849065..cfec4ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,10 +47,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@esbuild/linux-x64@0.25.10": +"@esbuild/win32-x64@0.25.10": version "0.25.10" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz" - integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA== + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz" + integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw== "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0", "@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.0" @@ -303,15 +303,15 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-linux-x64-gnu@4.52.0": +"@rollup/rollup-win32-x64-gnu@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz" - integrity sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA== + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz" + integrity sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw== -"@rollup/rollup-linux-x64-musl@4.52.0": +"@rollup/rollup-win32-x64-msvc@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz" - integrity sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ== + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz" + integrity sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -515,10 +515,10 @@ "@typescript-eslint/types" "8.44.0" eslint-visitor-keys "^4.2.1" -"@typescript/native-preview-linux-x64@7.0.0-dev.20250920.1": +"@typescript/native-preview-win32-x64@7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1" - resolved "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250920.1.tgz" - integrity sha512-6slYt8MKVAOZDbs2GXznPJ7KVmMLssqvmTJOmmNNyNJt/0MBJqwgL1b/TlxS9UgUTuBsNyKJbLtJgjvBVixIAA== + resolved "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250920.1.tgz" + integrity sha512-arxNEOP4LMr+rxCxbitG9ejEkmvvj7pkjMFTAFHfWJC0mKuT+1cQu79iZBhY16oHB4HLCziJRBRdMQhIB2Wf+w== "@typescript/native-preview@^7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1" From 09f7ed6d1fec4239aae3591fdf32694dc7ba5041 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:30:13 +0000 Subject: [PATCH 5/5] Refactor token-stream.ts to reduce cognitive complexity Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- src/parsing/token-stream.ts | 307 +++++++++++++++++++++++------------- yarn.lock | 24 +-- 2 files changed, 208 insertions(+), 123 deletions(-) diff --git a/src/parsing/token-stream.ts b/src/parsing/token-stream.ts index 86e4b6c..0cf450f 100644 --- a/src/parsing/token-stream.ts +++ b/src/parsing/token-stream.ts @@ -30,6 +30,25 @@ interface Coordinates { column: number; } +/** Single-character operators that map directly to themselves */ +const SINGLE_CHAR_OPERATORS = new Set(['+', '-', '*', '/', '%', '^', ':', '.']); + +/** Unicode multiplication symbols that map to '*' */ +const MULTIPLICATION_SYMBOLS = new Set(['∙', '•']); + +/** Escape sequence mappings for string unescape */ +const ESCAPE_SEQUENCES: Record = { + '\'': '\'', + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' +}; + export class TokenStream { public pos: number = 0; public keywords: string[]; @@ -269,6 +288,31 @@ export class TokenStream { private static readonly codePointPattern = /^[0-9a-f]{4}$/i; + /** + * Process a single escape sequence character and return the unescaped result + */ + private processEscapeChar(c: string, v: string, currentIndex: number): { char: string; skip: number } { + // Check standard escape sequences first + if (c in ESCAPE_SEQUENCES) { + return { char: ESCAPE_SEQUENCES[c], skip: 0 }; + } + + // Handle unicode escape sequence + if (c === 'u') { + const codePoint = v.substring(currentIndex + 1, currentIndex + 5); + if (!TokenStream.codePointPattern.test(codePoint)) { + this.parseError('Illegal escape sequence: \\u' + codePoint); + } + return { char: String.fromCharCode(parseInt(codePoint, 16)), skip: 4 }; + } + + // Unknown escape sequence + throw this.parseError('Illegal escape sequence: "\\' + c + '"'); + } + + /** + * Unescape a string by processing escape sequences + */ unescape(v: string): string { const index = v.indexOf('\\'); if (index < 0) { @@ -277,48 +321,13 @@ export class TokenStream { let buffer = v.substring(0, index); let currentIndex = index; + while (currentIndex >= 0) { const c = v.charAt(++currentIndex); - switch (c) { - case '\'': - buffer += '\''; - break; - case '"': - buffer += '"'; - break; - case '\\': - buffer += '\\'; - break; - case '/': - buffer += '/'; - break; - case 'b': - buffer += '\b'; - break; - case 'f': - buffer += '\f'; - break; - case 'n': - buffer += '\n'; - break; - case 'r': - buffer += '\r'; - break; - case 't': - buffer += '\t'; - break; - case 'u': - // interpret the following 4 characters as the hex of the unicode code point - const codePoint = v.substring(currentIndex + 1, currentIndex + 5); - if (!TokenStream.codePointPattern.test(codePoint)) { - this.parseError('Illegal escape sequence: \\u' + codePoint); - } - buffer += String.fromCharCode(parseInt(codePoint, 16)); - currentIndex += 4; - break; - default: - throw this.parseError('Illegal escape sequence: "\\' + c + '"'); - } + const { char, skip } = this.processEscapeChar(c, v, currentIndex); + buffer += char; + currentIndex += skip; + ++currentIndex; const backslash = v.indexOf('\\', currentIndex); buffer += v.substring(currentIndex, backslash < 0 ? v.length : backslash); @@ -441,88 +450,164 @@ export class TokenStream { return valid; } - isOperator(): boolean { - const startPos = this.pos; - const c = this.expression.charAt(this.pos); - - if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === ':' || c === '.') { + /** + * Try to match a single-character operator (+, -, *, /, %, ^, :, .) + */ + private tryMatchSingleCharOperator(c: string): boolean { + if (SINGLE_CHAR_OPERATORS.has(c)) { this.current = this.newToken(TOP, c); - } else if (c === '?') { - // ? could be a ternary operator a ? b : c or a coalesce operator a ?? b, we need to look ahead - // to figure out which one it is. - if (this.expression.charAt(this.pos + 1) === '?') { - if (this.isOperatorEnabled('??')) { - this.current = this.newToken(TOP, '??'); - this.pos++; - } else { - // We have a ?? operator but it has been disabled. - return false; - } - } else { - this.current = this.newToken(TOP, c); - } - } else if (c === '∙' || c === '•') { + return true; + } + return false; + } + + /** + * Try to match unicode multiplication symbols (∙, •) + */ + private tryMatchMultiplicationSymbol(c: string): boolean { + if (MULTIPLICATION_SYMBOLS.has(c)) { this.current = this.newToken(TOP, '*'); - } else if (c === '>') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '>='); - this.pos++; - } else { - this.current = this.newToken(TOP, '>'); - } - } else if (c === '<') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '<='); - this.pos++; - } else { - this.current = this.newToken(TOP, '<'); - } - } else if (c === '|') { - if (this.expression.charAt(this.pos + 1) === '|') { - this.current = this.newToken(TOP, '||'); - this.pos++; - } else { - this.current = this.newToken(TOP, '|'); - } - } else if (c === '&') { - if (this.expression.charAt(this.pos + 1) === '&') { - this.current = this.newToken(TOP, '&&'); - this.pos++; - } else { - return false; - } - } else if (c === '=') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '=='); - this.pos++; - } else { - this.current = this.newToken(TOP, c); - } - } else if (c === '!') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '!='); - this.pos++; - } else { - this.current = this.newToken(TOP, c); - } - } else if (c === 'a' && this.expression.substring(this.pos, this.pos + 3) === 'as ') { - if (this.isOperatorEnabled('as')) { - this.current = this.newToken(TOP, 'as'); - this.pos++; - } else { + return true; + } + return false; + } + + /** + * Try to match the question mark operator (? or ??) + * Returns null if no match, false if disabled, true if matched + */ + private tryMatchQuestionOperator(c: string): boolean | null { + if (c !== '?') { + return null; + } + + if (this.expression.charAt(this.pos + 1) === '?') { + if (!this.isOperatorEnabled('??')) { return false; } + this.current = this.newToken(TOP, '??'); + this.pos++; + } else { + this.current = this.newToken(TOP, c); + } + return true; + } + + /** + * Try to match a two-character operator where the second char may be '=' + * Examples: >=, <=, ==, != + */ + private tryMatchComparisonOperator(c: string, doubleOp: string): boolean { + if (this.expression.charAt(this.pos + 1) === '=') { + this.current = this.newToken(TOP, doubleOp); + this.pos++; + } else { + this.current = this.newToken(TOP, c); + } + return true; + } + + /** + * Try to match pipe operators (| or ||) + */ + private tryMatchPipeOperator(): boolean { + if (this.expression.charAt(this.pos + 1) === '|') { + this.current = this.newToken(TOP, '||'); + this.pos++; } else { + this.current = this.newToken(TOP, '|'); + } + return true; + } + + /** + * Try to match the && operator (single & is not valid) + */ + private tryMatchAmpersandOperator(): boolean | null { + if (this.expression.charAt(this.pos + 1) === '&') { + this.current = this.newToken(TOP, '&&'); + this.pos++; + return true; + } + return false; + } + + /** + * Try to match the 'as' keyword operator + */ + private tryMatchAsOperator(): boolean | null { + if (this.expression.substring(this.pos, this.pos + 3) !== 'as ') { + return null; + } + if (!this.isOperatorEnabled('as')) { return false; } + this.current = this.newToken(TOP, 'as'); this.pos++; + return true; + } - if (this.isOperatorEnabled(this.current.value as string)) { - return true; - } else { - this.pos = startPos; + /** + * Attempt to identify the current character as an operator + */ + isOperator(): boolean { + const startPos = this.pos; + const c = this.expression.charAt(this.pos); + + // Try single-character operators + if (this.tryMatchSingleCharOperator(c)) { + // matched + } + // Try unicode multiplication symbols + else if (this.tryMatchMultiplicationSymbol(c)) { + // matched + } + // Try question mark operators (? and ??) + else if (c === '?') { + const result = this.tryMatchQuestionOperator(c); + if (result === false) return false; + } + // Try comparison operators with optional '=' + else if (c === '>') { + this.tryMatchComparisonOperator(c, '>='); + } + else if (c === '<') { + this.tryMatchComparisonOperator(c, '<='); + } + else if (c === '=') { + this.tryMatchComparisonOperator(c, '=='); + } + else if (c === '!') { + this.tryMatchComparisonOperator(c, '!='); + } + // Try pipe operators + else if (c === '|') { + this.tryMatchPipeOperator(); + } + // Try ampersand operator + else if (c === '&') { + const result = this.tryMatchAmpersandOperator(); + if (result === false) return false; + } + // Try 'as' keyword operator + else if (c === 'a') { + const result = this.tryMatchAsOperator(); + if (!result) return false; + } + // No operator matched + else { return false; } + + this.pos++; + + // All successful branches above set this.current, so it's safe to access + if (this.current && this.isOperatorEnabled(this.current.value as string)) { + return true; + } + + this.pos = startPos; + return false; } isOperatorEnabled(op: string): boolean { diff --git a/yarn.lock b/yarn.lock index cfec4ad..8849065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,10 +47,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@esbuild/win32-x64@0.25.10": +"@esbuild/linux-x64@0.25.10": version "0.25.10" - resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz" - integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw== + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz" + integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA== "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0", "@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.0" @@ -303,15 +303,15 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-win32-x64-gnu@4.52.0": +"@rollup/rollup-linux-x64-gnu@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.0.tgz" - integrity sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw== + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.0.tgz" + integrity sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA== -"@rollup/rollup-win32-x64-msvc@4.52.0": +"@rollup/rollup-linux-x64-musl@4.52.0": version "4.52.0" - resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.0.tgz" - integrity sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ== + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.0.tgz" + integrity sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -515,10 +515,10 @@ "@typescript-eslint/types" "8.44.0" eslint-visitor-keys "^4.2.1" -"@typescript/native-preview-win32-x64@7.0.0-dev.20250920.1": +"@typescript/native-preview-linux-x64@7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1" - resolved "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250920.1.tgz" - integrity sha512-arxNEOP4LMr+rxCxbitG9ejEkmvvj7pkjMFTAFHfWJC0mKuT+1cQu79iZBhY16oHB4HLCziJRBRdMQhIB2Wf+w== + resolved "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250920.1.tgz" + integrity sha512-6slYt8MKVAOZDbs2GXznPJ7KVmMLssqvmTJOmmNNyNJt/0MBJqwgL1b/TlxS9UgUTuBsNyKJbLtJgjvBVixIAA== "@typescript/native-preview@^7.0.0-dev.20250920.1": version "7.0.0-dev.20250920.1"