From 594fa6eddb025151f86c7e58f660344504325d97 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 14:18:46 -0500 Subject: [PATCH 01/26] Phase 1: flatten token/node locations into pos/end/source Tokens and AST nodes now implement `Locatable` directly (`pos`, `end`, `source`) instead of carrying a `location: Location` object tree. - `Locatable` + `SourceInfo` types in Token.ts - Lexer emits offsets and builds `lineStarts` in one pass (drops line/column tracking) - AstNode declares abstract `pos`/`end`/`source`; removes `location` and `range` - node constructors use `util.setBounds()` instead of `createBoundingLocation()` - `util.getLocation()` builds a `Location` on demand - creators take a `Locatable` to copy from instead of a `Location` Consumers are updated in the next commit, so this one does not compile on its own. Co-Authored-By: Claude Opus 5.5 --- src/astUtils/creators.ts | 54 ++--- src/lexer/Lexer.ts | 109 ++-------- src/lexer/Token.ts | 36 +++- src/parser/AstNode.ts | 32 ++- src/parser/Expression.ts | 254 ++++++++++++++-------- src/parser/Parser.ts | 28 +-- src/parser/Statement.ts | 447 ++++++++++++++++++++++----------------- src/util.ts | 89 +++++--- 8 files changed, 582 insertions(+), 467 deletions(-) diff --git a/src/astUtils/creators.ts b/src/astUtils/creators.ts index 2df713ea6..0bbd7425c 100644 --- a/src/astUtils/creators.ts +++ b/src/astUtils/creators.ts @@ -1,5 +1,5 @@ import type { Location } from 'vscode-languageserver'; -import type { Identifier, Token } from '../lexer/Token'; +import type { Identifier, Locatable, Token } from '../lexer/Token'; import type { SGToken } from '../parser/SGTypes'; import { SGAttribute, SGComponent, SGInterface, SGInterfaceField, SGInterfaceFunction, SGScript } from '../parser/SGTypes'; import { TokenKind } from '../lexer/TokenKind'; @@ -77,37 +77,41 @@ const tokenDefaults = { [TokenKind.Whitespace]: ' ' }; -export function createToken(kind: T, text?: string, location?: Location): Token & { kind: T } { +export function createToken(kind: T, text?: string, locatable?: Locatable): Token & { kind: T } { return { kind: kind, text: text ?? tokenDefaults[kind as string] ?? kind.toString().toLowerCase(), isReserved: !text || text === kind.toString(), - location: location, + pos: locatable?.pos, + end: locatable?.end, + source: locatable?.source, leadingTrivia: [] }; } -export function createIdentifier(name: string, location?: Location): Identifier { +export function createIdentifier(name: string, locatable?: Locatable): Identifier { return { kind: TokenKind.Identifier, text: name, isReserved: false, - location: location, + pos: locatable?.pos, + end: locatable?.end, + source: locatable?.source, leadingTrivia: [] }; } -export function createVariableExpression(ident: string, location?: Location): VariableExpression { - return new VariableExpression({ name: createToken(TokenKind.Identifier, ident, location) }); +export function createVariableExpression(ident: string, locatable?: Locatable): VariableExpression { + return new VariableExpression({ name: createToken(TokenKind.Identifier, ident, locatable) }); } -export function createDottedIdentifier(path: string[], location?: Location): DottedGetExpression { +export function createDottedIdentifier(path: string[], locatable?: Locatable): DottedGetExpression { const ident = path.pop(); - const obj = path.length > 1 ? createDottedIdentifier(path, location) : createVariableExpression(path[0], location); + const obj = path.length > 1 ? createDottedIdentifier(path, locatable) : createVariableExpression(path[0], locatable); return new DottedGetExpression({ obj: obj, - name: createToken(TokenKind.Identifier, ident, location), - dot: createToken(TokenKind.Dot, '.', location) + name: createToken(TokenKind.Identifier, ident, locatable), + dot: createToken(TokenKind.Dot, '.', locatable) }); } @@ -116,30 +120,30 @@ export function createDottedIdentifier(path: string[], location?: Location): Dot * Since brightscript doesn't support strings with quotes in them, we can safely auto-detect and wrap the value in quotes in this function. * @param value - the string value. (value will be wrapped in quotes if they are missing) */ -export function createStringLiteral(value: string, location?: Location) { +export function createStringLiteral(value: string, locatable?: Locatable) { //wrap the value in double quotes if (!value.startsWith('"') && !value.endsWith('"')) { value = '"' + value + '"'; } - return new LiteralExpression({ value: createToken(TokenKind.StringLiteral, value, location) }); + return new LiteralExpression({ value: createToken(TokenKind.StringLiteral, value, locatable) }); } -export function createIntegerLiteral(value: string, location?: Location) { - return new LiteralExpression({ value: createToken(TokenKind.IntegerLiteral, value, location) }); +export function createIntegerLiteral(value: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(TokenKind.IntegerLiteral, value, locatable) }); } -export function createFloatLiteral(value: string, location?: Location) { - return new LiteralExpression({ value: createToken(TokenKind.FloatLiteral, value, location) }); +export function createFloatLiteral(value: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(TokenKind.FloatLiteral, value, locatable) }); } -export function createDoubleLiteral(value: string, location?: Location) { - return new LiteralExpression({ value: createToken(TokenKind.DoubleLiteral, value, location) }); +export function createDoubleLiteral(value: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(TokenKind.DoubleLiteral, value, locatable) }); } -export function createLongIntegerLiteral(value: string, location?: Location) { - return new LiteralExpression({ value: createToken(TokenKind.LongIntegerLiteral, value, location) }); +export function createLongIntegerLiteral(value: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(TokenKind.LongIntegerLiteral, value, locatable) }); } -export function createInvalidLiteral(value?: string, location?: Location) { - return new LiteralExpression({ value: createToken(TokenKind.Invalid, value, location) }); +export function createInvalidLiteral(value?: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(TokenKind.Invalid, value, locatable) }); } -export function createBooleanLiteral(value: string, location?: Location) { - return new LiteralExpression({ value: createToken(value === 'true' ? TokenKind.True : TokenKind.False, value, location) }); +export function createBooleanLiteral(value: string, locatable?: Locatable) { + return new LiteralExpression({ value: createToken(value === 'true' ? TokenKind.True : TokenKind.False, value, locatable) }); } export function createFunctionExpression(kind: TokenKind.Sub | TokenKind.Function) { return new FunctionExpression({ diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts index b8e12873c..df3d6532c 100644 --- a/src/lexer/Lexer.ts +++ b/src/lexer/Lexer.ts @@ -1,8 +1,8 @@ /* eslint-disable func-names */ import { TokenKind, ReservedTokenKinds, Keywords, PreceedingRegexTypes, AllowedTriviaTokens, FixedTokenText, LexerTextCache, LEXER_TEXT_CACHE_MAX_ENTRIES } from './TokenKind'; -import type { Token } from './Token'; +import type { SourceInfo, Token } from './Token'; import { isAlpha, isDecimalDigit, isAlphaNumeric, isHexDigit } from './Characters'; -import type { Location, Position } from 'vscode-languageserver'; +import type { Location } from 'vscode-languageserver'; import { DiagnosticMessages } from '../DiagnosticMessages'; import util from '../util'; import type { BsDiagnostic } from '../interfaces'; @@ -23,26 +23,6 @@ export class Lexer { */ private current: number; - /** - * The zero-indexed begin line number being parsed. - */ - private lineBegin: number; - - /** - * The zero-indexed end line number being parsed - */ - private lineEnd: number; - - /** - * The zero-indexed begin column number being parsed. - */ - private columnBegin: number; - - /** - * The zero-indexed end column number being parsed - */ - private columnEnd: number; - /** * The BrightScript code being converted to an array of `Token`s. */ @@ -69,9 +49,9 @@ export class Lexer { private leadingTrivia: Token[] = []; /** - * URI of the file being scanned (if available) + * Info about the source being scanned, shared by every token produced by this scan. `undefined` when locations aren't being tracked */ - private uri?: string; + private sourceInfo: SourceInfo | undefined; /** * A convenience function, equivalent to `new Lexer().scan(toScan)`, that converts a string @@ -99,14 +79,11 @@ export class Lexer { this.options = this.sanitizeOptions(options); this.start = 0; this.current = 0; - this.lineBegin = 0; - this.lineEnd = 0; - this.columnBegin = 0; - this.columnEnd = 0; this.tokens = []; this.diagnostics = []; - this.previousEndPosition = undefined; - this.uri = util.pathToUri(options?.srcPath); + this.sourceInfo = this.options.trackLocations + ? { uri: util.pathToUri(options?.srcPath), lineStarts: [0] } + : undefined; while (!this.isAtEnd()) { this.scanToken(); } @@ -115,9 +92,9 @@ export class Lexer { kind: TokenKind.Eof, text: '', isReserved: false, - location: this.options.trackLocations - ? util.createLocation(this.lineBegin, this.columnBegin, this.lineEnd, this.columnEnd + 1, this.uri) - : undefined, + pos: this.current, + end: this.current + 1, + source: this.sourceInfo, leadingTrivia: this.leadingTrivia.length > 0 ? this.leadingTrivia : undefined }); return this; @@ -430,12 +407,7 @@ export class Lexer { this.addToken(TokenKind.Newline); this.start = this.current; - // advance the line counter - this.lineBegin++; - this.lineEnd = this.lineBegin; - // and always reset the column counter - this.columnBegin = 0; - this.columnEnd = 0; + this.sourceInfo?.lineStarts.push(this.current); } /** @@ -443,20 +415,14 @@ export class Lexer { */ private advance(): void { this.current++; - this.columnEnd++; } - private lookaheadStack = [] as Array<{ current: number; columnEnd: number }>; + private lookaheadStack = [] as number[]; private pushLookahead() { - this.lookaheadStack.push({ - current: this.current, - columnEnd: this.columnEnd - }); + this.lookaheadStack.push(this.current); } private popLookahead() { - const { current, columnEnd } = this.lookaheadStack.pop(); - this.current = current; - this.columnEnd = columnEnd; + this.current = this.lookaheadStack.pop(); } /** @@ -556,11 +522,8 @@ export class Lexer { //store the char code token.charCode = 10; - //move the location tracking to the next line - this.lineEnd++; - this.lineBegin = this.lineEnd; - this.columnEnd = 0; - this.columnBegin = this.columnEnd; + //track the start of the next line + this.sourceInfo?.lineStarts.push(this.current); continue; } else if (this.check('\r') && this.peekNext() === '\n') { this.templateQuasiString(); @@ -573,11 +536,8 @@ export class Lexer { token = this.addToken(TokenKind.EscapedCharCodeLiteral) as Token & { charCode: number }; token.charCode = 10; - //move the location tracking to the next line - this.lineEnd++; - this.lineBegin = this.lineEnd; - this.columnEnd = 0; - this.columnBegin = this.columnEnd; + //track the start of the next line + this.sourceInfo?.lineStarts.push(this.current); continue; //escaped chars @@ -838,7 +798,6 @@ export class Lexer { (this.peek() === ' ' || this.peek() === '\t') ) { let savedCurrent = this.current; - let savedColumnEnd = this.columnEnd; // skip past any whitespace let whitespace = ''; @@ -861,22 +820,17 @@ export class Lexer { } else { // reset if the last word and the current word didn't form a multi-word TokenKind this.current = savedCurrent; - this.columnEnd = savedColumnEnd; } } // split `elseif` into `else` and `if` tokens if (lowerText === 'elseif' && !this.checkPreviousToken(TokenKind.Dot)) { let savedCurrent = this.current; - let savedColumnEnd = this.columnEnd; this.current -= 2; - this.columnEnd -= 2; this.addToken(TokenKind.Else); this.start = savedCurrent - 2; this.current = savedCurrent; - this.columnBegin = savedColumnEnd - 2; - this.columnEnd = savedColumnEnd; this.addToken(TokenKind.If); return; } @@ -1199,7 +1153,9 @@ export class Lexer { kind: kind, text: text, isReserved: ReservedTokenKinds.has(kind), - location: this.locationOf(), + pos: this.start, + end: this.current, + source: this.sourceInfo, leadingTrivia: undefined }; @@ -1217,12 +1173,10 @@ export class Lexer { } /** - * Move all location and char pointers to current position. Normally called after adding a token. + * Move the start pointer to the current position. Normally called after adding a token. */ private sync() { this.start = this.current; - this.lineBegin = this.lineEnd; - this.columnBegin = this.columnEnd; } /** @@ -1230,25 +1184,8 @@ export class Lexer { * @returns the location of `text` */ private locationOf(): Location { - if (this.options.trackLocations) { - //tokens (including trivia) are almost always back to back, so share the previous token's end position - //as this token's start instead of making a new one. Positions are never mutated in place - let start = this.previousEndPosition; - if (start?.line !== this.lineBegin || start?.character !== this.columnBegin) { - start = { line: this.lineBegin, character: this.columnBegin }; - } - const end = { line: this.lineEnd, character: this.columnEnd }; - this.previousEndPosition = end; - return { - uri: this.uri, - range: { start: start, end: end } - }; - } else { - return undefined; - } + return util.getLocation({ pos: this.start, end: this.current, source: this.sourceInfo }); } - - private previousEndPosition: Position; } export interface ScanOptions { diff --git a/src/lexer/Token.ts b/src/lexer/Token.ts index 8c6174c47..df4adf3c1 100644 --- a/src/lexer/Token.ts +++ b/src/lexer/Token.ts @@ -1,10 +1,9 @@ import { TokenKind } from './TokenKind'; -import type { Location } from 'vscode-languageserver'; /** * Represents a chunk of BrightScript scanned by the lexer. */ -export interface Token { +export interface Token extends Locatable { /** * The type of token this represents. */ @@ -18,10 +17,6 @@ export interface Token { * */ isReserved?: boolean; - /** - * Where the token was found. - */ - location: Location; /** * Any tokens starting on the next line of the previous token, up to the start of this token */ @@ -29,10 +24,35 @@ export interface Token { } /** - * Any object that has a range + * Any object that has a location in a source file. Use `util.getLocation()` to get the line/character `Location` */ export interface Locatable { - location: Location; + /** + * Absolute UTF-16 offset into the source text where this item starts + */ + pos: number; + /** + * Absolute UTF-16 offset into the source text where this item ends (exclusive) + */ + end: number; + /** + * Info about the source this item was parsed from. Shared by every item from the same parse. `undefined` for synthetic items + */ + source: SourceInfo | undefined; +} + +/** + * Info about a parsed source, shared by every `Locatable` produced from the same parse + */ +export interface SourceInfo { + /** + * The uri of the file + */ + uri: string; + /** + * The offset of the start of each line + */ + lineStarts: number[]; } /** diff --git a/src/parser/AstNode.ts b/src/parser/AstNode.ts index fc1524776..f524d66db 100644 --- a/src/parser/AstNode.ts +++ b/src/parser/AstNode.ts @@ -1,34 +1,34 @@ import type { WalkVisitor, WalkOptions } from '../astUtils/visitors'; import { WalkMode } from '../astUtils/visitors'; -import type { Location, Position, Range } from 'vscode-languageserver'; +import type { Position } from 'vscode-languageserver'; import { CancellationTokenSource } from 'vscode-languageserver'; import { InternalWalkMode } from '../astUtils/visitors'; import type { SymbolTable } from '../SymbolTable'; import type { BrsTranspileState } from './BrsTranspileState'; import type { GetTypeOptions, TranspileResult } from '../interfaces'; import type { AnnotationExpression } from './Expression'; -import util from '../util'; import { DynamicType } from '../types/DynamicType'; import type { BscType } from '../types/BscType'; -import type { Token } from '../lexer/Token'; +import type { Locatable, SourceInfo, Token } from '../lexer/Token'; import { isBlock, isBody, isFunctionParameterExpression } from '../astUtils/reflection'; /** * A BrightScript AST node */ -export abstract class AstNode { +export abstract class AstNode implements Locatable { public abstract kind: AstNodeKind; /** - * The starting and ending location of the node. + * Absolute offset where this node starts in its source. Use `util.getLocation()` to get the line/character `Location` */ - public abstract location?: Location | undefined; - + public abstract pos: number; /** - * @deprecated use `location.range` instead + * Absolute offset where this node ends in its source (exclusive) */ - public get range(): Range | undefined { - return this.location?.range; - } + public abstract end: number; + /** + * Info about the source this node was parsed from. `undefined` for synthetic nodes + */ + public abstract source: SourceInfo | undefined; public abstract transpile(state: BrsTranspileState): TranspileResult; @@ -152,8 +152,9 @@ export abstract class AstNode { */ public findChildAtPosition(position: Position, options?: WalkOptions): TNodeType | undefined { return this.findChild((node) => { - //if the current node includes this range, keep that node - if (util.rangeContains(node?.location.range, position)) { + //if the current node includes this position, keep that node (nodes without a source include every position) + const offset = node?.source?.lineStarts[position.line] + position.character; + if (!node?.source || (node.pos <= offset && offset <= node.end)) { return node.findChildAtPosition(position, options) ?? node; } }, options); @@ -296,11 +297,6 @@ export abstract class AstNode { } } } - - //reapply the location if we have one but the clone doesn't - if (!clone.location && this.location) { - clone.location = util.cloneLocation(this.location); - } return clone; } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 6815cd2a2..64de258a4 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import type { Token, Identifier } from '../lexer/Token'; +import type { Token, Identifier, Locatable, SourceInfo } from '../lexer/Token'; import type { PrintSeparatorToken } from '../lexer/TokenKind'; import { TokenKind } from '../lexer/TokenKind'; import type { Block, NamespaceStatement } from './Statement'; @@ -50,7 +50,7 @@ export class BinaryExpression extends Expression { }; this.left = options.left; this.right = options.right; - this.location = util.createBoundingLocation(this.left, this.tokens.operator, this.right); + util.setBounds(this, this.left, this.tokens.operator, this.right); } readonly tokens: { readonly operator: Token; @@ -61,7 +61,9 @@ export class BinaryExpression extends Expression { public readonly kind = AstNodeKind.BinaryExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState): TranspileResult { return [ @@ -140,7 +142,7 @@ export class CallExpression extends Expression { }; this.callee = options.callee; this.args = options.args ?? []; - this.location = util.createBoundingLocation(this.callee, this.tokens.openingParen, ...this.args ?? [], this.tokens.closingParen); + util.setBounds(this, this.callee, this.tokens.openingParen, ...this.args ?? [], this.tokens.closingParen); } readonly callee: Expression; @@ -155,7 +157,9 @@ export class CallExpression extends Expression { public readonly kind = AstNodeKind.CallExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** `a.b()` -> `a.b` (not the args) */ public get previousInChain() { @@ -280,6 +284,15 @@ export class FunctionExpression extends Expression implements TypedefProvider { this.body.parent = this; } this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable()); + util.setBounds(this, + this.tokens.functionType, + this.tokens.leftParen, + ...this.parameters ?? [], + this.tokens.rightParen, + this.tokens.as, + this.returnTypeExpression, + this.tokens.endFunctionType + ); } public readonly kind = AstNodeKind.FunctionExpression; @@ -304,21 +317,9 @@ export class FunctionExpression extends Expression implements TypedefProvider { return this.tokens.endFunctionType?.leadingTrivia; } - /** - * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing), - * and ending with the last n' in 'end function' or 'b' in 'end sub' - */ - public get location(): Location { - return util.createBoundingLocation( - this.tokens.functionType, - this.tokens.leftParen, - ...this.parameters ?? [], - this.tokens.rightParen, - this.tokens.as, - this.returnTypeExpression, - this.tokens.endFunctionType - ); - } + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) { let results = [] as TranspileResult; @@ -535,6 +536,13 @@ export class FunctionParameterExpression extends Expression { }; this.defaultValue = options.defaultValue; this.typeExpression = options.typeExpression; + util.setBounds(this, + this.tokens.name, + this.tokens.as, + this.typeExpression, + this.tokens.equals, + this.defaultValue + ); } public readonly kind = AstNodeKind.FunctionParameterExpression; @@ -569,15 +577,9 @@ export class FunctionParameterExpression extends Expression { return paramType; } - public get location(): Location | undefined { - return util.createBoundingLocation( - this.tokens.name, - this.tokens.as, - this.typeExpression, - this.tokens.equals, - this.defaultValue - ); - } + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState) { let result: TranspileResult = [ @@ -664,7 +666,7 @@ export class DottedGetExpression extends Expression { }; this.obj = options.obj; - this.location = util.createBoundingLocation(this.obj, this.tokens.dot, this.tokens.name); + util.setBounds(this, this.obj, this.tokens.dot, this.tokens.name); } readonly tokens: { @@ -689,7 +691,9 @@ export class DottedGetExpression extends Expression { public readonly kind = AstNodeKind.DottedGetExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** `a.b` -> `a` */ public get previousInChain() { @@ -785,7 +789,7 @@ export class XmlAttributeGetExpression extends Expression { super(); this.obj = options.obj; this.tokens = { at: options.at, name: options.name }; - this.location = util.createBoundingLocation(this.obj, this.tokens.at, this.tokens.name); + util.setBounds(this, this.obj, this.tokens.at, this.tokens.name); } public readonly kind = AstNodeKind.XmlAttributeGetExpression; @@ -797,7 +801,9 @@ export class XmlAttributeGetExpression extends Expression { public readonly obj: Expression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** `a@b` -> `a` */ public get previousInChain() { @@ -853,7 +859,7 @@ export class IndexedGetExpression extends Expression { }; this.obj = options.obj; this.indexes = options.indexes; - this.location = util.createBoundingLocation( + util.setBounds(this, this.obj, this.tokens.openingSquare, this.tokens.questionDot, @@ -898,7 +904,9 @@ export class IndexedGetExpression extends Expression { return this.indexes?.[0]; } - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** `a[i]` -> `a` (not the index) */ public get previousInChain() { @@ -974,7 +982,7 @@ export class GroupingExpression extends Expression { leftParen: options.leftParen }; this.expression = options.expression; - this.location = util.createBoundingLocation(this.tokens.leftParen, this.expression, this.tokens.rightParen); + util.setBounds(this, this.tokens.leftParen, this.expression, this.tokens.rightParen); } public readonly tokens: { @@ -985,7 +993,9 @@ export class GroupingExpression extends Expression { public readonly kind = AstNodeKind.GroupingExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { if (isTypecastExpression(this.expression)) { @@ -1040,8 +1050,14 @@ export class LiteralExpression extends Expression { public readonly kind = AstNodeKind.LiteralExpression; - public get location() { - return this.tokens.value.location; + public get pos() { + return this.tokens.value.pos; + } + public get end() { + return this.tokens.value.end; + } + public get source() { + return this.tokens.value.source; } public getType(options?: GetTypeOptions) { @@ -1098,7 +1114,7 @@ export class PrintSeparatorExpression extends Expression { this.tokens = { separator: options.separator }; - this.location = this.tokens.separator.location; + util.setBounds(this, this.tokens.separator); } public readonly tokens: { @@ -1107,7 +1123,9 @@ export class PrintSeparatorExpression extends Expression { public readonly kind = AstNodeKind.PrintSeparatorExpression; - public location: Location; + public pos: number; + public end: number; + public source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -1142,7 +1160,7 @@ export class EscapedCharCodeLiteralExpression extends Expression { }) { super(); this.tokens = { value: options.value }; - this.location = util.cloneLocation(this.tokens.value.location); + util.setBounds(this, this.tokens.value); } public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression; @@ -1151,7 +1169,9 @@ export class EscapedCharCodeLiteralExpression extends Expression { readonly value: Token & { charCode: number }; }; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -1184,7 +1204,7 @@ export class ArrayLiteralExpression extends Expression { close: options.close }; this.elements = options.elements; - this.location = util.createBoundingLocation(this.tokens.open, ...this.elements ?? [], this.tokens.close); + util.setBounds(this, this.tokens.open, ...this.elements ?? [], this.tokens.close); } public readonly elements: Array; @@ -1196,7 +1216,9 @@ export class ArrayLiteralExpression extends Expression { public readonly kind = AstNodeKind.ArrayLiteralExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result: TranspileResult = []; @@ -1275,12 +1297,14 @@ export class AAMemberExpression extends Expression { comma: options.comma }; this.value = options.value; - this.location = util.createBoundingLocation(this.tokens.key, this.tokens.colon, this.value); + util.setBounds(this, this.tokens.key, this.tokens.colon, this.value); } public readonly kind = AstNodeKind.AAMemberExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly key: Token; @@ -1338,7 +1362,7 @@ export class AAIndexedMemberExpression extends Expression { comma: options.comma }; this.value = options.value; - this.location = util.createBoundingLocation(this.tokens.leftBracket, this.key, this.tokens.rightBracket, this.tokens.colon, this.value, this.tokens.comma); + util.setBounds(this, this.tokens.leftBracket, this.key, this.tokens.rightBracket, this.tokens.colon, this.value, this.tokens.comma); } public readonly tokens: { @@ -1355,7 +1379,9 @@ export class AAIndexedMemberExpression extends Expression { public readonly kind = AstNodeKind.AAIndexedMemberExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return []; @@ -1394,7 +1420,7 @@ export class AALiteralExpression extends Expression { close: options.close }; this.elements = options.elements; - this.location = util.createBoundingLocation(this.tokens.open, ...this.elements ?? [], this.tokens.close); + util.setBounds(this, this.tokens.open, ...this.elements ?? [], this.tokens.close); } public readonly elements: Array; @@ -1405,7 +1431,9 @@ export class AALiteralExpression extends Expression { public readonly kind = AstNodeKind.AALiteralExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result: TranspileResult = []; @@ -1517,12 +1545,14 @@ export class UnaryExpression extends Expression { operator: options.operator }; this.right = options.right; - this.location = util.createBoundingLocation(this.tokens.operator, this.right); + util.setBounds(this, this.tokens.operator, this.right); } public readonly kind = AstNodeKind.UnaryExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly operator: Token; @@ -1579,7 +1609,7 @@ export class VariableExpression extends Expression { this.tokens = { name: options.name }; - this.location = util.cloneLocation(this.tokens.name?.location); + util.setBounds(this, this.tokens.name); } public readonly tokens: { @@ -1588,7 +1618,9 @@ export class VariableExpression extends Expression { public readonly kind = AstNodeKind.VariableExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * @deprecated use `tokens.name` instead @@ -1677,10 +1709,12 @@ export class SourceLiteralExpression extends Expression { this.tokens = { value: options.value }; - this.location = util.cloneLocation(this.tokens.value?.location); + util.setBounds(this, this.tokens.value); } - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly kind = AstNodeKind.SourceLiteralExpression; @@ -1823,12 +1857,14 @@ export class NewExpression extends Expression { new: options.new }; this.call = options.call; - this.location = util.createBoundingLocation(this.tokens.new, this.call); + util.setBounds(this, this.tokens.new, this.call); } public readonly kind = AstNodeKind.NewExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly new?: Token; @@ -1913,7 +1949,7 @@ export class CallfuncExpression extends Expression { this.callee = options.callee; this.args = options.args ?? []; - this.location = util.createBoundingLocation( + util.setBounds(this, this.callee, this.tokens.operator, this.tokens.methodName, @@ -1935,7 +1971,9 @@ export class CallfuncExpression extends Expression { public readonly kind = AstNodeKind.CallfuncExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** `a@.b()` -> `a` (not the args) */ public get previousInChain() { @@ -2013,7 +2051,7 @@ export class TemplateStringQuasiExpression extends Expression { }) { super(); this.expressions = options.expressions; - this.location = util.createBoundingLocation( + util.setBounds(this, ...this.expressions ?? [] ); } @@ -2021,7 +2059,9 @@ export class TemplateStringQuasiExpression extends Expression { public readonly expressions: Array; public readonly kind = AstNodeKind.TemplateStringQuasiExpression; - readonly location: Location | undefined; + readonly pos: number; + readonly end: number; + readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState, skipEmptyStrings = true) { let result = [] as TranspileResult; @@ -2071,7 +2111,7 @@ export class TemplateStringExpression extends Expression { }; this.quasis = options.quasis; this.expressions = options.expressions; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.openingBacktick, this.quasis?.[0], this.quasis?.[this.quasis?.length - 1], @@ -2088,7 +2128,9 @@ export class TemplateStringExpression extends Expression { public readonly quasis: TemplateStringQuasiExpression[]; public readonly expressions: Expression[]; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public getType(options: GetTypeOptions) { return StringType.instance; @@ -2192,7 +2234,7 @@ export class TaggedTemplateStringExpression extends Expression { this.quasis = options.quasis; this.expressions = options.expressions; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.tagName, this.tokens.openingBacktick, this.quasis?.[0], @@ -2212,7 +2254,9 @@ export class TaggedTemplateStringExpression extends Expression { public readonly quasis: TemplateStringQuasiExpression[]; public readonly expressions: Expression[]; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -2306,12 +2350,18 @@ export class AnnotationExpression extends Expression { readonly name: Token; }; - public get location(): Location | undefined { - return util.createBoundingLocation( - this.tokens.at, - this.tokens.name, - this.call - ); + public get pos() { + return this.getBounds().pos; + } + public get end() { + return this.getBounds().end; + } + public get source() { + return this.getBounds().source; + } + private getBounds() { + //this needs to be computed on demand because `call` is assigned after construction + return util.setBounds({} as Locatable, this.tokens.at, this.tokens.name, this.call); } public readonly name: string; @@ -2376,7 +2426,7 @@ export class TernaryExpression extends Expression { this.test = options.test; this.consequent = options.consequent; this.alternate = options.alternate; - this.location = util.createBoundingLocation( + util.setBounds(this, this.test, this.tokens.questionMark, this.consequent, @@ -2387,7 +2437,9 @@ export class TernaryExpression extends Expression { public readonly kind = AstNodeKind.TernaryExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly questionMark?: Token; @@ -2501,7 +2553,7 @@ export class NullCoalescingExpression extends Expression { }; this.consequent = options.consequent; this.alternate = options.alternate; - this.location = util.createBoundingLocation( + util.setBounds(this, this.consequent, this.tokens.questionQuestion, this.alternate @@ -2510,7 +2562,9 @@ export class NullCoalescingExpression extends Expression { public readonly kind = AstNodeKind.NullCoalescingExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly questionQuestion?: Token; @@ -2622,8 +2676,14 @@ export class RegexLiteralExpression extends Expression { readonly regexLiteral: Token; }; - public get location(): Location { - return this.tokens?.regexLiteral?.location; + public get pos() { + return this.tokens?.regexLiteral?.pos; + } + public get end() { + return this.tokens?.regexLiteral?.end; + } + public get source() { + return this.tokens?.regexLiteral?.source; } public transpile(state: BrsTranspileState): TranspileResult { @@ -2734,7 +2794,7 @@ export class TypeExpression extends Expression implements TypedefProvider { super(); this.expression = options.expression; this.resolvedType = options.resolvedType; - this.location = util.cloneLocation(this.expression?.location); + util.setBounds(this, this.expression); } public readonly kind = AstNodeKind.TypeExpression; @@ -2750,7 +2810,9 @@ export class TypeExpression extends Expression implements TypedefProvider { */ public readonly resolvedType?: BscType; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { //roku built-in names (rosgnode*, etc.) collapse to `dynamic` at transpile. @@ -2829,7 +2891,7 @@ export class TypecastExpression extends Expression { }; this.obj = options.obj; this.typeExpression = options.typeExpression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.obj, this.tokens.as, this.typeExpression @@ -2846,7 +2908,9 @@ export class TypecastExpression extends Expression { public typeExpression?: TypeExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { return this.obj.transpile(state); @@ -2894,7 +2958,7 @@ export class TypedArrayExpression extends Expression { rightBracket: options.rightBracket }; this.innerType = options.innerType; - this.location = util.createBoundingLocation( + util.setBounds(this, this.innerType, this.tokens.leftBracket, this.tokens.rightBracket @@ -2910,7 +2974,9 @@ export class TypedArrayExpression extends Expression { public readonly kind = AstNodeKind.TypedArrayExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { //typed arrays (e.g. `float[]`, `float[][][]`) collapse to `dynamic` since @@ -2952,7 +3018,7 @@ export class InlineInterfaceExpression extends Expression { close: options.close }; this.members = options.members; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.open, ...this.members, this.tokens.close @@ -2968,7 +3034,9 @@ export class InlineInterfaceExpression extends Expression { public readonly kind = AstNodeKind.InlineInterfaceExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; get leadingTrivia(): Token[] { return this.tokens.open?.leadingTrivia; @@ -3029,7 +3097,7 @@ export class InlineInterfaceMemberExpression extends Expression { optional: options.optional }; this.typeExpression = options.typeExpression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.optional, this.tokens.name, this.tokens.as, @@ -3047,7 +3115,9 @@ export class InlineInterfaceMemberExpression extends Expression { public readonly typeExpression?: TypeExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; get leadingTrivia(): Token[] { return this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia; @@ -3103,7 +3173,7 @@ export class TypedFunctionTypeExpression extends Expression { }; this.params = options.params; this.returnType = options.returnType; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.functionType, this.tokens.leftParen, ...this.params, @@ -3125,7 +3195,9 @@ export class TypedFunctionTypeExpression extends Expression { public readonly params: FunctionParameterExpression[]; public readonly returnType?: TypeExpression; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()]; diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 58e5b491a..d1c6572b2 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -921,7 +921,9 @@ export class Parser { text: 'function', isReserved: true, //zero-length location means derived - location: this.peek().location, + pos: this.peek().pos, + end: this.peek().end, + source: this.peek().source, leadingTrivia: [] }; } @@ -1376,23 +1378,13 @@ export class Parser { const exitText = exitToken.text.substring(0, 4); const whileText = exitToken.text.substring(4); - const originalRange = exitToken.location?.range; - const originalStart = originalRange?.start; - - const exitRange = util.createRange( - originalStart.line, - originalStart.character, - originalStart.line, - originalStart.character + 4); - const whileRange = util.createRange( - originalStart.line, - originalStart.character + 4, - originalStart.line, - originalStart.character + exitToken.text.length); - - exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange)); + const originalToken = exitToken; + + exitToken = createToken(TokenKind.Exit, exitText, originalToken); + exitToken.end = exitToken.pos + 4; this.tokens[this.current - 1] = exitToken; - const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange)); + const newLoopToken = createToken(TokenKind.While, whileText, originalToken); + newLoopToken.pos += 4; this.tokens.splice(this.current, 0, newLoopToken); } @@ -1561,8 +1553,6 @@ export class Parser { endNamespace: endKeyword }); - //cache the range property so that plugins can't affect it - result.cacheLocation(); result.body.symbolTable.name += `: namespace '${result.name}'`; return result; } diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 42e391728..fea4837c5 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import type { Token, Identifier } from '../lexer/Token'; +import type { Token, Identifier, Locatable, SourceInfo } from '../lexer/Token'; import { TokenKind } from '../lexer/TokenKind'; import type { DottedGetExpression, LiteralExpression, TypecastExpression } from './Expression'; import { FunctionExpression, FunctionParameterExpression, TypeExpression } from './Expression'; @@ -33,12 +33,14 @@ export class EmptyStatement extends Statement { constructor(options?: { range?: Location } ) { super(); - this.location = undefined; + util.setBounds(this); } /** * Create a negative range to indicate this is an interpolated location */ - public readonly location?: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly kind = AstNodeKind.EmptyStatement; @@ -51,9 +53,7 @@ export class EmptyStatement extends Statement { public clone() { return this.finalizeClone( - new EmptyStatement({ - range: util.cloneLocation(this.location) - }) + new EmptyStatement() ); } } @@ -74,19 +74,23 @@ export class Body extends Statement implements TypedefProvider { public readonly symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable()); - public get location() { - if (!this._location) { - //this needs to be a getter because the body has its statements pushed to it after being constructed - this._location = util.createBoundingLocation( - ...(this.statements ?? []) - ); - } - return this._location; + public get pos() { + return this.getBounds().pos; } - public set location(value) { - this._location = value; + public get end() { + return this.getBounds().end; + } + public get source() { + return this.getBounds().source; + } + private getBounds() { + //this needs to be lazy because the body has its statements pushed to it after being constructed + if (!this._bounds?.source) { + this._bounds = util.setBounds(this._bounds ?? {} as Locatable, ...(this.statements ?? [])); + } + return this._bounds; } - private _location: Location; + private _bounds: Locatable; transpile(state: BrsTranspileState) { let result: TranspileResult = state.transpileAnnotations(this); @@ -167,7 +171,7 @@ export class AssignmentStatement extends Statement { as: options.as }; this.typeExpression = options.typeExpression; - this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.value); + util.setBounds(this, ...Object.values(this.tokens), this.value); } public readonly tokens: { @@ -182,7 +186,9 @@ export class AssignmentStatement extends Statement { public readonly kind = AstNodeKind.AssignmentStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -244,7 +250,7 @@ export class AugmentedAssignmentStatement extends Statement { }; this.item = options.item; this.value = options.value; - this.location = util.createBoundingLocation(this.item, util.createBoundingLocationFromTokens(this.tokens), this.value); + util.setBounds(this, this.item, ...Object.values(this.tokens), this.value); } public readonly tokens: { @@ -257,7 +263,9 @@ export class AugmentedAssignmentStatement extends Statement { public readonly kind = AstNodeKind.AugmentedAssignmentStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -316,15 +324,15 @@ export class Block extends Statement { public readonly kind = AstNodeKind.Block; - private buildLocation(): Location { + private buildBounds(): Locatable { if (this.statements?.length > 0) { - return util.createBoundingLocation(...this.statements ?? []); + return util.setBounds({} as Locatable, ...this.statements ?? []); } - let lastBitBefore: Location; - let firstBitAfter: Location; + let lastBitBefore: Locatable; + let firstBitAfter: Locatable; if (isFunctionExpression(this.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.tokens.functionType, this.parent.tokens.leftParen, ...(this.parent.parameters ?? []), @@ -332,40 +340,40 @@ export class Block extends Statement { this.parent.tokens.as, this.parent.returnTypeExpression ); - firstBitAfter = this.parent.tokens.endFunctionType?.location; + firstBitAfter = this.parent.tokens.endFunctionType; } else if (isIfStatement(this.parent)) { if (this.parent.thenBranch === this) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.tokens.then, this.parent.condition ); - firstBitAfter = util.createBoundingLocation( + firstBitAfter = util.setBounds({} as Locatable, this.parent.tokens.else, this.parent.elseBranch, this.parent.tokens.endIf ); } else if (this.parent.elseBranch === this) { - lastBitBefore = this.parent.tokens.else?.location; - firstBitAfter = this.parent.tokens.endIf?.location; + lastBitBefore = this.parent.tokens.else; + firstBitAfter = this.parent.tokens.endIf; } } else if (isConditionalCompileStatement(this.parent)) { if (this.parent.thenBranch === this) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.tokens.condition, this.parent.tokens.not, this.parent.tokens.hashIf ); - firstBitAfter = util.createBoundingLocation( + firstBitAfter = util.setBounds({} as Locatable, this.parent.tokens.hashElse, this.parent.elseBranch, this.parent.tokens.hashEndIf ); } else if (this.parent.elseBranch === this) { - lastBitBefore = this.parent.tokens.hashElse?.location; - firstBitAfter = this.parent.tokens.hashEndIf?.location; + lastBitBefore = this.parent.tokens.hashElse; + firstBitAfter = this.parent.tokens.hashEndIf; } } else if (isForStatement(this.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.increment, this.parent.tokens.step, this.parent.finalValue, @@ -373,58 +381,58 @@ export class Block extends Statement { this.parent.counterDeclaration, this.parent.tokens.for ); - firstBitAfter = this.parent.tokens.endFor?.location; + firstBitAfter = this.parent.tokens.endFor; } else if (isForEachStatement(this.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.target, this.parent.tokens.in, this.parent.tokens.item, this.parent.tokens.forEach ); - firstBitAfter = this.parent.tokens.endFor?.location; + firstBitAfter = this.parent.tokens.endFor; } else if (isWhileStatement(this.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.condition, this.parent.tokens.while ); - firstBitAfter = this.parent.tokens.endWhile?.location; + firstBitAfter = this.parent.tokens.endWhile; } else if (isTryCatchStatement(this.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.tokens.try ); - firstBitAfter = util.createBoundingLocation( + firstBitAfter = util.setBounds({} as Locatable, this.parent.tokens.endTry, this.parent.catchStatement ); } else if (isCatchStatement(this.parent) && isTryCatchStatement(this.parent?.parent)) { - lastBitBefore = util.createBoundingLocation( + lastBitBefore = util.setBounds({} as Locatable, this.parent.tokens.catch, this.parent.exceptionVariableExpression ); - firstBitAfter = this.parent.parent.tokens.endTry?.location; - } - if (lastBitBefore?.range && firstBitAfter?.range) { - return util.createLocation( - lastBitBefore.range.end.line, - lastBitBefore.range.end.character, - firstBitAfter.range.start.line, - firstBitAfter.range.start.character, - lastBitBefore.uri ?? firstBitAfter.uri - ); + firstBitAfter = this.parent.parent.tokens.endTry; + } + if (lastBitBefore?.source && firstBitAfter?.source) { + return { pos: lastBitBefore.end, end: firstBitAfter.pos, source: lastBitBefore.source }; } } - public get location() { - if (!this._location) { - //this needs to be a getter because the body has its statements pushed to it after being constructed - this._location = this.buildLocation(); - } - return this._location; + public get pos() { + return this.getBounds()?.pos; } - public set location(value) { - this._location = value; + public get end() { + return this.getBounds()?.end; + } + public get source() { + return this.getBounds()?.source; + } + private getBounds() { + //this needs to be lazy because the body has its statements pushed to it after being constructed + if (!this._bounds?.source) { + this._bounds = this.buildBounds(); + } + return this._bounds; } - private _location: Location; + private _bounds: Locatable; transpile(state: BrsTranspileState) { state.blockDepth++; @@ -498,12 +506,14 @@ export class ExpressionStatement extends Statement { }) { super(); this.expression = options.expression; - this.location = this.expression?.location; + util.setBounds(this, this.expression); } public readonly expression: Expression; public readonly kind = AstNodeKind.ExpressionStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -548,7 +558,7 @@ export class ExitStatement extends Statement { exit: options?.exit, loopType: options.loopType }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.exit, this.tokens.loopType ); @@ -561,7 +571,9 @@ export class ExitStatement extends Statement { public readonly kind = AstNodeKind.ExitStatement; - public readonly location?: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -603,7 +615,7 @@ export class FunctionStatement extends Statement implements TypedefProvider { this.func.symbolTable.name += `: '${this.tokens.name?.text}'`; } - this.location = this.func?.location; + util.setBounds(this, this.func); } public readonly tokens: { @@ -613,7 +625,9 @@ export class FunctionStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.FunctionStatement as AstNodeKind; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * Get the name of this expression based on the parse mode @@ -715,8 +729,8 @@ export class IfStatement extends Statement { endIf: options.endIf }; - this.location = util.createBoundingLocation( - util.createBoundingLocationFromTokens(this.tokens), + util.setBounds(this, + ...Object.values(this.tokens), this.condition, this.thenBranch, this.elseBranch @@ -735,7 +749,9 @@ export class IfStatement extends Statement { public readonly kind = AstNodeKind.IfStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; get isInline() { const allLeadingTrivia = [ @@ -857,7 +873,7 @@ export class IncrementStatement extends Statement { this.tokens = { operator: options.operator }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.value, this.tokens.operator ); @@ -870,7 +886,9 @@ export class IncrementStatement extends Statement { public readonly kind = AstNodeKind.IncrementStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -919,7 +937,7 @@ export class PrintStatement extends Statement { print: options.print }; this.expressions = options.expressions; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.print, ...(this.expressions ?? []) ); @@ -933,7 +951,9 @@ export class PrintStatement extends Statement { public readonly kind = AstNodeKind.PrintStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [ @@ -1000,7 +1020,7 @@ export class DimStatement extends Statement { closingSquare: options.closingSquare }; this.dimensions = options.dimensions; - this.location = util.createBoundingLocation( + util.setBounds(this, options.dim, options.name, options.openingSquare, @@ -1019,7 +1039,9 @@ export class DimStatement extends Statement { public readonly kind = AstNodeKind.DimStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState) { let result: TranspileResult = [ @@ -1084,7 +1106,7 @@ export class GotoStatement extends Statement { goto: options.goto, label: options.label }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.goto, this.tokens.label ); @@ -1097,7 +1119,9 @@ export class GotoStatement extends Statement { public readonly kind = AstNodeKind.GotoStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -1135,7 +1159,7 @@ export class LabelStatement extends Statement { name: options.name, colon: options.colon }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.name, this.tokens.colon ); @@ -1146,7 +1170,9 @@ export class LabelStatement extends Statement { }; public readonly kind = AstNodeKind.LabelStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public get leadingTrivia(): Token[] { return this.tokens.name.leadingTrivia; @@ -1184,7 +1210,7 @@ export class ReturnStatement extends Statement { return: options?.return }; this.value = options?.value; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.return, this.value ); @@ -1196,7 +1222,9 @@ export class ReturnStatement extends Statement { public readonly value?: Expression; public readonly kind = AstNodeKind.ReturnStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -1239,14 +1267,16 @@ export class EndStatement extends Statement { this.tokens = { end: options?.end }; - this.location = this.tokens.end?.location; + util.setBounds(this, this.tokens.end); } public readonly tokens: { readonly end?: Token; }; public readonly kind = AstNodeKind.EndStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -1277,7 +1307,7 @@ export class StopStatement extends Statement { }) { super(); this.tokens = { stop: options?.stop }; - this.location = this.tokens?.stop?.location; + util.setBounds(this, this.tokens?.stop); } public readonly tokens: { readonly stop?: Token; @@ -1285,7 +1315,9 @@ export class StopStatement extends Statement { public readonly kind = AstNodeKind.StopStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -1333,7 +1365,7 @@ export class ForStatement extends Statement { this.body = options.body; this.increment = options.increment; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.for, this.counterDeclaration, this.tokens.to, @@ -1359,7 +1391,9 @@ export class ForStatement extends Statement { public readonly kind = AstNodeKind.ForStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -1463,7 +1497,7 @@ export class ForEachStatement extends Statement { this.target = options.target; this.typeExpression = options.typeExpression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.forEach, this.tokens.item, this.tokens.as, @@ -1488,7 +1522,9 @@ export class ForEachStatement extends Statement { public readonly kind = AstNodeKind.ForEachStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -1586,7 +1622,7 @@ export class WhileStatement extends Statement { }; this.body = options.body; this.condition = options.condition; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.while, this.condition, this.body, @@ -1603,7 +1639,9 @@ export class WhileStatement extends Statement { public readonly kind = AstNodeKind.WhileStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -1675,7 +1713,7 @@ export class DottedSetStatement extends Statement { }; this.obj = options.obj; this.value = options.value; - this.location = util.createBoundingLocation( + util.setBounds(this, this.obj, this.tokens.dot, this.tokens.equals, @@ -1694,7 +1732,9 @@ export class DottedSetStatement extends Statement { public readonly kind = AstNodeKind.DottedSetStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that @@ -1768,7 +1808,7 @@ export class IndexedSetStatement extends Statement { this.obj = options.obj; this.indexes = options.indexes ?? []; this.value = options.value; - this.location = util.createBoundingLocation( + util.setBounds(this, this.obj, this.tokens.openingSquare, ...this.indexes, @@ -1788,7 +1828,9 @@ export class IndexedSetStatement extends Statement { public readonly kind = AstNodeKind.IndexedSetStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { const result = []; @@ -1856,7 +1898,7 @@ export class LibraryStatement extends Statement implements TypedefProvider { library: options?.library, filePath: options?.filePath }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.library, this.tokens.filePath ); @@ -1868,7 +1910,9 @@ export class LibraryStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.LibraryStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let result = [] as TranspileResult; @@ -1921,6 +1965,7 @@ export class NamespaceStatement extends Statement implements TypedefProvider { }; this.nameExpression = options.nameExpression; this.body = options.body; + util.setBounds(this, this.tokens.namespace, this.nameExpression, this.body, this.tokens.endNamespace); this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.getRoot()?.getSymbolTable()); } @@ -1941,22 +1986,9 @@ export class NamespaceStatement extends Statement implements TypedefProvider { return this.getName(ParseMode.BrighterScript); } - public get location() { - return this.cacheLocation(); - } - private _location: Location | undefined; - - public cacheLocation() { - if (!this._location) { - this._location = util.createBoundingLocation( - this.tokens.namespace, - this.nameExpression, - this.body, - this.tokens.endNamespace - ); - } - return this._location; - } + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public getName(parseMode: ParseMode) { const sep = parseMode === ParseMode.BrighterScript ? '.' : '_'; @@ -2046,7 +2078,6 @@ export class NamespaceStatement extends Statement implements TypedefProvider { }), ['nameExpression', 'body'] ); - clone.cacheLocation(); return clone; } } @@ -2061,22 +2092,17 @@ export class ImportStatement extends Statement implements TypedefProvider { import: options.import, path: options.path }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.import, this.tokens.path ); if (this.tokens.path) { //remove quotes this.filePath = this.tokens.path.text.replace(/"/g, ''); - if (this.tokens.path?.location?.range) { + if (this.tokens.path.source) { //adjust the range to exclude the quotes - this.tokens.path.location = util.createLocation( - this.tokens.path.location.range.start.line, - this.tokens.path.location.range.start.character + 1, - this.tokens.path.location.range.end.line, - this.tokens.path.location.range.end.character - 1, - this.tokens.path.location.uri - ); + this.tokens.path.pos++; + this.tokens.path.end--; } } } @@ -2088,7 +2114,9 @@ export class ImportStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.ImportStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly filePath: string; @@ -2150,7 +2178,7 @@ export class InterfaceStatement extends Statement implements TypedefProvider { }; this.parentInterfaceName = options.parentInterfaceName; this.body = options.body; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.interface, this.tokens.name, this.tokens.extends, @@ -2171,7 +2199,9 @@ export class InterfaceStatement extends Statement implements TypedefProvider { readonly endInterface?: Token; }; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public get fields(): InterfaceFieldStatement[] { return this.body.filter(x => isInterfaceFieldStatement(x)) as InterfaceFieldStatement[]; @@ -2365,7 +2395,7 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide as: options.as }; this.typeExpression = options.typeExpression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.optional, this.tokens.name, this.tokens.as, @@ -2377,7 +2407,9 @@ export class InterfaceFieldStatement extends Statement implements TypedefProvide public readonly typeExpression?: TypeExpression; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public readonly tokens: { readonly name: Identifier; @@ -2482,12 +2514,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid }; this.params = options.params ?? []; this.returnTypeExpression = options.returnTypeExpression; - } - - public readonly kind = AstNodeKind.InterfaceMethodStatement; - - public get location() { - return util.createBoundingLocation( + util.setBounds(this, this.tokens.optional, this.tokens.functionType, this.tokens.name, @@ -2498,6 +2525,12 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid this.returnTypeExpression ); } + + public readonly kind = AstNodeKind.InterfaceMethodStatement; + + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * Get the name of this method. */ @@ -2673,10 +2706,10 @@ export class ClassStatement extends Statement implements TypedefProvider { this.registerMembers(this.body); - this.location = util.createBoundingLocation( + util.setBounds(this, this.parentClassName, ...(this.body ?? []), - util.createBoundingLocationFromTokens(this.tokens) + ...Object.values(this.tokens) ); } @@ -2725,7 +2758,9 @@ export class ClassStatement extends Statement implements TypedefProvider { public readonly methods = [] as MethodStatement[]; public readonly fields = [] as FieldStatement[]; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * Register all members (methods and fields) found in the given statements, @@ -3348,9 +3383,9 @@ export class MethodStatement extends FunctionStatement { ...this.tokens, override: options.override }; - this.location = util.createBoundingLocation( + util.setBounds(this, ...(this.modifiers), - util.createBoundingLocationFromTokens(this.tokens), + ...Object.values(this.tokens), this.func ); } @@ -3368,7 +3403,9 @@ export class MethodStatement extends FunctionStatement { return this.modifiers.find(x => accessModifiers.includes(x.kind)); } - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * Get the name of this method. @@ -3467,7 +3504,9 @@ export class MethodStatement extends FunctionStatement { kind: TokenKind.Identifier, text: 'super', isReserved: false, - location: state.classStatement.tokens.name.location, + pos: state.classStatement.tokens.name.pos, + end: state.classStatement.tokens.name.end, + source: state.classStatement.tokens.name.source, leadingTrivia: [] } }), @@ -3475,14 +3514,18 @@ export class MethodStatement extends FunctionStatement { kind: TokenKind.LeftParen, text: '(', isReserved: false, - location: state.classStatement.tokens.name.location, + pos: state.classStatement.tokens.name.pos, + end: state.classStatement.tokens.name.end, + source: state.classStatement.tokens.name.source, leadingTrivia: [] }, closingParen: { kind: TokenKind.RightParen, text: ')', isReserved: false, - location: state.classStatement.tokens.name.location, + pos: state.classStatement.tokens.name.pos, + end: state.classStatement.tokens.name.end, + source: state.classStatement.tokens.name.source, leadingTrivia: [] }, args: [] @@ -3527,10 +3570,10 @@ export class MethodStatement extends FunctionStatement { value: field.initialValue }) : new AssignmentStatement({ - equals: createToken(TokenKind.Equal, '=', field.tokens.name.location), + equals: createToken(TokenKind.Equal, '=', field.tokens.name), name: thisQualifiedName, //if there is no initial value, set the initial value to `invalid` - value: createInvalidLiteral('invalid', field.tokens.name.location) + value: createInvalidLiteral('invalid', field.tokens.name) }); // Add parent so namespace lookups work fieldAssignment.parent = state.classStatement; @@ -3636,8 +3679,8 @@ export class FieldStatement extends Statement implements TypedefProvider { this.typeExpression = options.typeExpression; this.initialValue = options.initialValue; - this.location = util.createBoundingLocation( - util.createBoundingLocationFromTokens(this.tokens), + util.setBounds(this, + ...Object.values(this.tokens), this.typeExpression, this.initialValue ); @@ -3672,7 +3715,9 @@ export class FieldStatement extends Statement implements TypedefProvider { DynamicType.instance; } - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public get leadingTrivia(): Token[] { return this.tokens.accessModifier?.leadingTrivia ?? this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia; @@ -3763,7 +3808,7 @@ export class TryCatchStatement extends Statement { }; this.tryBranch = options.tryBranch; this.catchStatement = options.catchStatement; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.try, this.tryBranch, this.catchStatement, @@ -3781,7 +3826,9 @@ export class TryCatchStatement extends Statement { public readonly kind = AstNodeKind.TryCatchStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { return [ @@ -3836,7 +3883,7 @@ export class CatchStatement extends Statement { }; this.exceptionVariableExpression = options?.exceptionVariableExpression; this.catchBranch = options?.catchBranch; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.catch, this.exceptionVariableExpression, this.catchBranch @@ -3853,7 +3900,9 @@ export class CatchStatement extends Statement { public readonly kind = AstNodeKind.CatchStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState): TranspileResult { return [ @@ -3901,7 +3950,7 @@ export class ThrowStatement extends Statement { throw: options.throw }; this.expression = options.expression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.throw, this.expression ); @@ -3914,7 +3963,9 @@ export class ThrowStatement extends Statement { public readonly kind = AstNodeKind.ThrowStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public transpile(state: BrsTranspileState) { const result = [ @@ -3972,6 +4023,12 @@ export class EnumStatement extends Statement implements TypedefProvider { }; this.symbolTable = new SymbolTable('Enum'); this.body = options.body ?? []; + util.setBounds(this, + this.tokens.enum, + this.tokens.name, + ...this.body, + this.tokens.endEnum + ); } public readonly tokens: { @@ -3983,14 +4040,9 @@ export class EnumStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.EnumStatement; - public get location(): Location | undefined { - return util.createBoundingLocation( - this.tokens.enum, - this.tokens.name, - ...this.body, - this.tokens.endEnum - ); - } + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public getMembers() { const result = [] as EnumMemberStatement[]; @@ -4173,6 +4225,11 @@ export class EnumMemberStatement extends Statement implements TypedefProvider { equals: options.equals }; this.value = options.value; + util.setBounds(this, + this.tokens.name, + this.tokens.equals, + this.value + ); } public readonly tokens: { @@ -4183,13 +4240,9 @@ export class EnumMemberStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.EnumMemberStatement; - public get location() { - return util.createBoundingLocation( - this.tokens.name, - this.tokens.equals, - this.value - ); - } + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; /** * The name of the member @@ -4278,7 +4331,7 @@ export class ConstStatement extends Statement implements TypedefProvider { equals: options.equals }; this.value = options.value; - this.location = util.createBoundingLocation(this.tokens.const, this.tokens.name, this.tokens.equals, this.value); + util.setBounds(this, this.tokens.const, this.tokens.name, this.tokens.equals, this.value); } public readonly tokens: { @@ -4290,7 +4343,9 @@ export class ConstStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.ConstStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; public get name() { return this.tokens.name.text; @@ -4382,7 +4437,7 @@ export class ContinueStatement extends Statement { continue: options.continue, loopType: options.loopType }; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.continue, this.tokens.loopType ); @@ -4395,7 +4450,9 @@ export class ContinueStatement extends Statement { public readonly kind = AstNodeKind.ContinueStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { //when targeting firmware without native `continue` support, rewrite into a jump to the @@ -4450,7 +4507,7 @@ export class TypecastStatement extends Statement { typecast: options.typecast }; this.typecastExpression = options.typecastExpression; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.typecast, this.typecastExpression ); @@ -4464,7 +4521,9 @@ export class TypecastStatement extends Statement { public readonly kind = AstNodeKind.TypecastStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { //the typecast statement is a comment just for debugging purposes @@ -4514,7 +4573,7 @@ export class ConditionalCompileErrorStatement extends Statement { hashError: options.hashError, message: options.message }; - this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens)); + util.setBounds(this, ...Object.values(this.tokens)); } public readonly tokens: { @@ -4525,7 +4584,9 @@ export class ConditionalCompileErrorStatement extends Statement { public readonly kind = AstNodeKind.ConditionalCompileErrorStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -4569,7 +4630,7 @@ export class AliasStatement extends Statement { equals: options.equals }; this.value = options.value; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.alias, this.tokens.name, this.tokens.equals, @@ -4589,7 +4650,9 @@ export class AliasStatement extends Statement { public readonly kind = AstNodeKind.AliasStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { //transpile to a comment just for debugging purposes @@ -4653,8 +4716,8 @@ export class ConditionalCompileStatement extends Statement { hashEndIf: options.hashEndIf }; - this.location = util.createBoundingLocation( - util.createBoundingLocationFromTokens(this.tokens), + util.setBounds(this, + ...Object.values(this.tokens), this.thenBranch, this.elseBranch ); @@ -4672,7 +4735,9 @@ export class ConditionalCompileStatement extends Statement { public readonly kind = AstNodeKind.ConditionalCompileStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { let results = [] as TranspileResult; @@ -4803,7 +4868,7 @@ export class ConditionalCompileConstStatement extends Statement { hashConst: options.hashConst }; this.assignment = options.assignment; - this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.assignment); + util.setBounds(this, ...Object.values(this.tokens), this.assignment); } public readonly tokens: { @@ -4814,7 +4879,9 @@ export class ConditionalCompileConstStatement extends Statement { public readonly kind = AstNodeKind.ConditionalCompileConstStatement; - public readonly location: Location | undefined; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { return [ @@ -4865,7 +4932,7 @@ export class TypeStatement extends Statement implements TypedefProvider { equals: options.equals }; this.value = options.value; - this.location = util.createBoundingLocation( + util.setBounds(this, this.tokens.type, this.tokens.name, this.tokens.equals, @@ -4883,7 +4950,9 @@ export class TypeStatement extends Statement implements TypedefProvider { public readonly kind = AstNodeKind.TypeStatement; - public readonly location: Location; + public readonly pos: number; + public readonly end: number; + public readonly source: SourceInfo | undefined; transpile(state: BrsTranspileState) { //type statements have no runtime representation, so they're stripped entirely diff --git a/src/util.ts b/src/util.ts index cd7942f93..0ad4db9e5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -24,7 +24,7 @@ import { VoidType } from './types/VoidType'; import { ParseMode } from './parser/Parser'; import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionParameterExpression, IndexedGetExpression, LiteralExpression, TypeExpression, VariableExpression } from './parser/Expression'; import { LogLevel, createLogger } from './logging'; -import { isToken, type Identifier, type Token } from './lexer/Token'; +import { isToken, type Identifier, type Locatable, type SourceInfo, type Token } from './lexer/Token'; import { TokenKind } from './lexer/TokenKind'; import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isCompoundType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isParamTypeFromValueReferenceType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile, isArrayType, isAssociativeArrayTypeLike, isBuiltInType, isTypedFunctionTypeLike, isGroupingExpression, isInlineInterfaceExpression, isTypedFunctionTypeExpression } from './astUtils/reflection'; import { WalkMode } from './astUtils/visitors'; @@ -1069,6 +1069,8 @@ export class Util { public extractRange(rangeIsh: RangeLike): Range | undefined { if (!rangeIsh) { return undefined; + } else if ('source' in rangeIsh) { + return this.getLocation(rangeIsh)?.range; } else if ('location' in rangeIsh) { return rangeIsh.location?.range; } else if ('range' in rangeIsh) { @@ -1265,7 +1267,9 @@ export class Util { kind: token.kind, text: token.text, isReserved: token.isReserved, - location: this.cloneLocation(token.location), + pos: token.pos, + end: token.end, + source: token.source, leadingTrivia: token.leadingTrivia ? token.leadingTrivia.map(x => this.cloneToken(x)) : undefined } as Token; //handle those tokens that have charCode @@ -1343,40 +1347,63 @@ export class Util { } /** - * Gets the bounding range of an object that contains a bunch of tokens - * @param tokens Object with tokens in it - * @returns Range containing all the tokens + * Set the `pos`, `end`, and `source` of `target` to the bounds of all the locatables. + * Offsets from different sources can't be compared, so only locatables from the same source as the first one are included */ - public createBoundingLocationFromTokens(tokens: Record): Location | undefined { - let uri: string; - let startPosition: Position | undefined; - let endPosition: Position | undefined; - for (let key in tokens) { - let token = tokens?.[key]; - let locatableRange = token?.location?.range; - if (!locatableRange) { + public setBounds(target: T, ...locatables: Array): T { + let pos: number; + let end: number; + let source: SourceInfo; + for (const locatable of locatables) { + if (!locatable?.source) { continue; - } - - if (!startPosition) { - startPosition = locatableRange.start; - } else if (this.comparePosition(locatableRange.start, startPosition) < 0) { - startPosition = locatableRange.start; - } - if (!endPosition) { - endPosition = locatableRange.end; - } else if (this.comparePosition(locatableRange.end, endPosition) > 0) { - endPosition = locatableRange.end; - } - if (!uri) { - uri = token.location.uri; + } else if (!source) { + source = locatable.source; + pos = locatable.pos; + end = locatable.end; + } else if (locatable.source === source) { + pos = Math.min(pos, locatable.pos); + end = Math.max(end, locatable.end); } } - if (startPosition && endPosition) { - return this.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri); - } else { + target.pos = pos; + target.end = end; + target.source = source; + return target; + } + + /** + * Build a line/character `Location` for the locatable. Returns `undefined` for synthetic items (those without a `source`) + */ + public getLocation(locatable: Locatable): Location | undefined { + const source = locatable?.source; + if (!source) { return undefined; } + const lineStarts = source.lineStarts; + const startLine = this.getLineIndex(lineStarts, locatable.pos, 0); + //`end` is exclusive, so use the line of the last character. This keeps a token that ends with a newline on its own line + const endLine = locatable.end > locatable.pos ? this.getLineIndex(lineStarts, locatable.end - 1, startLine) : startLine; + return { + uri: source.uri, + range: this.createRange(startLine, locatable.pos - lineStarts[startLine], endLine, locatable.end - lineStarts[endLine]) + }; + } + + /** + * Binary search `lineStarts` (beginning at line index `low`) for the index of the line that contains `offset` + */ + private getLineIndex(lineStarts: number[], offset: number, low: number) { + let high = lineStarts.length - 1; + while (low < high) { + const mid = Math.floor((low + high + 1) / 2); + if (lineStarts[mid] <= offset) { + low = mid; + } else { + high = mid - 1; + } + } + return low; } /** @@ -3210,7 +3237,7 @@ export function standardizePath(stringParts: TemplateStringsArray | string, ...e /** * An item that can be coerced into a `Range` */ -export type RangeLike = { location?: Location } | Location | { range?: Range } | Range | undefined; +export type RangeLike = Locatable | { location?: Location } | Location | { range?: Range } | Range | undefined; export let util = new Util(); export default util; From a0a91d4d73a8b8983b0a3a83b27365751db2e543 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 14:21:43 -0500 Subject: [PATCH 02/26] Phase 2: read locations on demand with util.getLocation() Everything that read `.location` off a token or AST node (diagnostics, LSP processors, validators, BrsFile lookups, etc.) now calls `util.getLocation()` to build the `Location` at that boundary. Comparisons still happen on the resulting line/character ranges, so behavior is unchanged. - `util.createBoundingLocation()` accepts `Locatable` items too - `TypeChainEntry` keeps a `locatable` and builds its `location` lazily Transpile/sourcemap consumers are updated in the next commit. Co-Authored-By: Claude Opus 5.5 --- src/CrossScopeValidator.ts | 4 +- src/FunctionScope.ts | 3 +- src/astUtils/CachedLookups.ts | 2 +- src/bscPlugin/CallExpressionInfo.ts | 4 +- src/bscPlugin/SignatureHelpUtil.ts | 6 +- .../codeActions/CodeActionsProcessor.ts | 32 ++-- .../completions/CompletionsProcessor.ts | 18 +-- .../definition/DefinitionProvider.ts | 30 ++-- src/bscPlugin/hover/HoverProcessor.ts | 4 +- .../inlayHints/InlayHintProcessor.ts | 6 +- .../references/ReferencesProvider.ts | 4 +- .../SelectionRangesProcessor.ts | 7 +- .../BrsFileSemanticTokensProcessor.ts | 4 +- src/bscPlugin/symbols/symbolUtils.ts | 22 +-- .../transpile/BrsFileTranspileProcessor.ts | 34 ++--- src/bscPlugin/validation/BrsFileValidator.ts | 60 ++++---- src/bscPlugin/validation/ScopeValidator.ts | 88 +++++------ src/files/BrsFile.ts | 48 +++--- src/interfaces.ts | 14 +- src/lsp/Project.ts | 2 +- src/parser/BrightScriptDocParser.ts | 16 +- src/parser/Expression.ts | 6 +- src/parser/Parser.ts | 140 +++++++++--------- src/parser/Statement.ts | 8 +- src/util.ts | 14 +- src/validators/ClassValidator.ts | 17 ++- 26 files changed, 303 insertions(+), 290 deletions(-) diff --git a/src/CrossScopeValidator.ts b/src/CrossScopeValidator.ts index cb80fb5b2..939154055 100644 --- a/src/CrossScopeValidator.ts +++ b/src/CrossScopeValidator.ts @@ -861,8 +861,8 @@ export class CrossScopeValidator { type AstNodeWithName = VariableExpression | DottedGetExpression | EnumStatement | ClassStatement | ConstStatement | EnumMemberStatement | InterfaceStatement; const thatNodeKindName = otherIsGlobal ? 'Global Function' : util.getAstNodeFriendlyName(otherDupeNode) ?? 'Item'; - let thisNameRange = (dupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? dupeNode.location?.range; - let thatNameRange = (otherDupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? otherDupeNode?.location?.range; + let thisNameRange = util.getLocation((dupeNode as AstNodeWithName)?.tokens?.name)?.range ?? util.getLocation(dupeNode)?.range; + let thatNameRange = util.getLocation((otherDupeNode as AstNodeWithName)?.tokens?.name)?.range ?? util.getLocation(otherDupeNode)?.range; const relatedInformation = thatNameRange ? [{ message: `${thatNodeKindName} declared here`, diff --git a/src/FunctionScope.ts b/src/FunctionScope.ts index e1af48c26..550cea7e0 100644 --- a/src/FunctionScope.ts +++ b/src/FunctionScope.ts @@ -1,5 +1,6 @@ import type { LabelDeclaration, VariableDeclaration } from './interfaces'; import type { FunctionExpression } from './parser/Expression'; +import { util } from './util'; //TODO I think this class can be eliminated in favor of moving some of these onto the FunctionExpression AST node export class FunctionScope { @@ -13,7 +14,7 @@ export class FunctionScope { * and ends after the final `n` in `end function` or `b` in end sub. */ public get range() { - return this.func?.location?.range; + return util.getLocation(this.func)?.range; } /** * The scopes that are children of this scope diff --git a/src/astUtils/CachedLookups.ts b/src/astUtils/CachedLookups.ts index 915c2c1e8..69649774d 100644 --- a/src/astUtils/CachedLookups.ts +++ b/src/astUtils/CachedLookups.ts @@ -240,7 +240,7 @@ export class CachedLookups { node = node.obj; } else { //some expression we don't understand. log it and quit the loop - this.file.program.logger.debug('Encountered unknown expression while calculating function expression chain', node.kind, node.location); + this.file.program.logger.debug('Encountered unknown expression while calculating function expression chain', node.kind, util.getLocation(node)); break; } } diff --git a/src/bscPlugin/CallExpressionInfo.ts b/src/bscPlugin/CallExpressionInfo.ts index 6a563cb78..4d8170228 100644 --- a/src/bscPlugin/CallExpressionInfo.ts +++ b/src/bscPlugin/CallExpressionInfo.ts @@ -96,7 +96,7 @@ export class CallExpressionInfo { if (!this.callExpression) { return false; } - let boundingRange = util.createBoundingRange(this.callExpression.tokens.openingParen?.location, this.callExpression.tokens.closingParen?.location); + let boundingRange = util.createBoundingRange(util.getLocation(this.callExpression.tokens.openingParen), util.getLocation(this.callExpression.tokens.closingParen)); return util.rangeContains(boundingRange, this.position); } @@ -159,7 +159,7 @@ export class CallExpressionInfo { } for (let i = this.callExpression.args.length - 1; i > -1; i--) { let argExpression = this.callExpression.args[i]; - let comparison = util.comparePositionToRange(this.position, argExpression.location?.range); + let comparison = util.comparePositionToRange(this.position, util.getLocation(argExpression)?.range); if (comparison >= 0) { return i + comparison; } diff --git a/src/bscPlugin/SignatureHelpUtil.ts b/src/bscPlugin/SignatureHelpUtil.ts index d2075472f..697310cf9 100644 --- a/src/bscPlugin/SignatureHelpUtil.ts +++ b/src/bscPlugin/SignatureHelpUtil.ts @@ -75,7 +75,7 @@ export class SignatureHelpUtil { return undefined; } const func = statement.func; - const funcStartPosition = func.location?.range.start; + const funcStartPosition = util.getLocation(func)?.range.start; // Get function comments in reverse order const trivia = util.concatAnnotationLeadingTrivia(func).reverse(); @@ -85,7 +85,7 @@ export class SignatureHelpUtil { if (!currentToken) { break; } - if (currentToken.location?.range.start.line + 1 < funcStartPosition.line) { + if (util.getLocation(currentToken)?.range.start.line + 1 < funcStartPosition.line) { if (functionComments.length === 0) { break; } @@ -121,7 +121,7 @@ export class SignatureHelpUtil { key += param.tokens.name.text; } - let label = util.getTextForRange(lines, util.createRangeFromPositions(func.tokens.functionType?.location?.range.start, func.body.location?.range.start)).trim(); + let label = util.getTextForRange(lines, util.createRangeFromPositions(util.getLocation(func.tokens.functionType)?.range.start, util.getLocation(func.body)?.range.start)).trim(); if (namespaceName) { label = label.replace(/^(sub | function )/gim, `$1${namespaceName}.`); } diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.ts index cd76f53b4..64a85f2ff 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.ts @@ -260,7 +260,7 @@ export class CodeActionsProcessor { const commentTokens = isXml ? [token] : token.leadingTrivia.filter(t => t.kind === TokenKind.Comment); for (const commentToken of commentTokens) { - const tokenRange: Range = isXml ? rangeFromTokenValue(commentToken) : commentToken.location.range; + const tokenRange: Range = isXml ? rangeFromTokenValue(commentToken) : util.getLocation(commentToken).range; const tokenText: string = isXml ? commentToken.image : commentToken.text; const parsed = parseDisableComment(tokenText); if (!parsed) { @@ -348,7 +348,7 @@ export class CodeActionsProcessor { // eslint-disable-next-line @typescript-eslint/dot-notation const importStatements = (this.event.file as BrsFile)['_cachedLookups'].importStatements; //find the position of the first import statement, or the top of the file if there is none - const insertPosition = importStatements[importStatements.length - 1]?.tokens.import.location.range?.start ?? util.createPosition(0, 0); + const insertPosition = util.getLocation(importStatements[importStatements.length - 1]?.tokens.import)?.range?.start ?? util.createPosition(0, 0); //find all files that reference this function for (const file of files) { @@ -405,7 +405,7 @@ export class CodeActionsProcessor { const file = this.event.file; // eslint-disable-next-line @typescript-eslint/dot-notation const importStatements = file['_cachedLookups'].importStatements; - const insertPosition = importStatements[importStatements.length - 1]?.tokens.import.location.range?.start ?? util.createPosition(0, 0); + const insertPosition = util.getLocation(importStatements[importStatements.length - 1]?.tokens.import)?.range?.start ?? util.createPosition(0, 0); const changes: InsertChange[] = []; const addedPaths = new Set(); @@ -537,9 +537,9 @@ export class CodeActionsProcessor { kind: CodeActionKind.QuickFix, changes: [ //function - { type: 'replace', filePath: this.event.file.srcPath, range: func.tokens.functionType.location.range, newText: functionTypeText }, + { type: 'replace', filePath: this.event.file.srcPath, range: util.getLocation(func.tokens.functionType).range, newText: functionTypeText }, //end function - { type: 'replace', filePath: this.event.file.srcPath, range: func.tokens.endFunctionType.location.range, newText: endFunctionTypeText } + { type: 'replace', filePath: this.event.file.srcPath, range: util.getLocation(func.tokens.endFunctionType).range, newText: endFunctionTypeText } ] }) ); @@ -601,7 +601,7 @@ export class CodeActionsProcessor { if (!fn) { continue; } - const fnKey = `${fn.location.range.start.line}:${fn.location.range.start.character}`; + const fnKey = `${util.getLocation(fn).range.start.line}:${util.getLocation(fn).range.start.character}`; if (seenFunctions.has(fnKey)) { continue; } @@ -613,7 +613,7 @@ export class CodeActionsProcessor { addVoidChanges.push({ type: 'insert', filePath: this.event.file.srcPath, - position: fn.tokens.rightParen.location.range.end, + position: util.getLocation(fn.tokens.rightParen).range.end, newText: ` ${asText ?? 'as'} ${voidText ?? 'void'}` }); } @@ -642,8 +642,8 @@ export class CodeActionsProcessor { diagnostics: [diagnostics[0]], kind: CodeActionKind.QuickFix, changes: [ - { type: 'replace', filePath: file.srcPath, range: func.tokens.functionType.location.range, newText: subText ?? 'sub' }, - { type: 'replace', filePath: file.srcPath, range: func.tokens.endFunctionType.location.range, newText: endSubText ?? 'end sub' } + { type: 'replace', filePath: file.srcPath, range: util.getLocation(func.tokens.functionType).range, newText: subText ?? 'sub' }, + { type: 'replace', filePath: file.srcPath, range: util.getLocation(func.tokens.endFunctionType).range, newText: endSubText ?? 'end sub' } ] }) ); @@ -718,10 +718,10 @@ export class CodeActionsProcessor { file.ast.walk((node) => { if ( isMethodStatement(node) && - node.location?.range?.start?.line === diagnostic.location.range.start.line && - node.location?.range?.start?.character === diagnostic.location.range.start.character + util.getLocation(node)?.range?.start?.line === diagnostic.location.range.start.line && + util.getLocation(node)?.range?.start?.character === diagnostic.location.range.start.character ) { - insertPosition = (node as MethodStatement).func.tokens.functionType?.location?.range?.start; + insertPosition = util.getLocation((node as MethodStatement).func.tokens.functionType)?.range?.start; } }, { walkMode: WalkMode.visitStatementsRecursive }); @@ -819,10 +819,10 @@ export class CodeActionsProcessor { filePath: this.event.file.srcPath, // )| as | range: util.createRange( - func.tokens.rightParen.location.range.start.line, - func.tokens.rightParen.location.range.start.character + 1, - func.returnTypeExpression.location.range.end.line, - func.returnTypeExpression.location.range.end.character + util.getLocation(func.tokens.rightParen).range.start.line, + util.getLocation(func.tokens.rightParen).range.start.character + 1, + util.getLocation(func.returnTypeExpression).range.end.line, + util.getLocation(func.returnTypeExpression).range.end.character ) }; } diff --git a/src/bscPlugin/completions/CompletionsProcessor.ts b/src/bscPlugin/completions/CompletionsProcessor.ts index c32ff98b1..08658497a 100644 --- a/src/bscPlugin/completions/CompletionsProcessor.ts +++ b/src/bscPlugin/completions/CompletionsProcessor.ts @@ -178,7 +178,7 @@ export class CompletionsProcessor { public getBrsFileCompletions(position: Position, file: BrsFile, scopesToProcess: Scope[]): { global: CompletionItem[]; scoped: CompletionItem[] } { let result = [] as CompletionItem[]; const currentTokenByFilePosition = file.getTokenAt(position); - const currentToken = currentTokenByFilePosition ?? file.getTokenAt(file.getClosestExpression(position)?.location?.range.start); + const currentToken = currentTokenByFilePosition ?? file.getTokenAt(util.getLocation(file.getClosestExpression(position))?.range.start); const emptyResult = { global: [], scoped: [] }; @@ -206,12 +206,12 @@ export class CompletionsProcessor { if (this.isTokenAdjacentTo(file, currentToken, TokenKind.Dot)) { const dotToken = this.getAdjacentToken(file, currentToken, TokenKind.Dot); beforeDotToken = file.getTokenBefore(dotToken); - expression = file.getClosestExpression(beforeDotToken?.location?.range.end); + expression = file.getClosestExpression(util.getLocation(beforeDotToken)?.range.end); shouldLookForMembers = true; } else if (this.isTokenAdjacentTo(file, currentToken, TokenKind.Callfunc)) { const dotToken = this.getAdjacentToken(file, currentToken, TokenKind.Callfunc); beforeDotToken = file.getTokenBefore(dotToken); - expression = file.getClosestExpression(beforeDotToken?.location?.range.end); + expression = file.getClosestExpression(util.getLocation(beforeDotToken)?.range.end); shouldLookForCallFuncMembers = true; } else if (this.isTokenAdjacentTo(file, currentToken, TokenKind.As)) { if (file.parseMode === ParseMode.BrightScript) { @@ -239,7 +239,7 @@ export class CompletionsProcessor { return emptyResult; } - const tokenBefore = file.getTokenBefore(file.getClosestToken(expression.location?.range?.start)); + const tokenBefore = file.getTokenBefore(file.getClosestToken(util.getLocation(expression)?.range?.start)); // helper to check get correct symbol tables for look ups function getSymbolTableForLookups() { @@ -543,12 +543,12 @@ export class CompletionsProcessor { label: pkgPath, textEdit: TextEdit.replace( util.createRange( - currentToken.location?.range.start.line, + util.getLocation(currentToken)?.range.start.line, //+1 to step past the opening quote - currentToken.location?.range.start.character + (openingQuote ? 1 : 0), - currentToken.location?.range.end.line, + util.getLocation(currentToken)?.range.start.character + (openingQuote ? 1 : 0), + util.getLocation(currentToken)?.range.end.line, //-1 to exclude the closing quotemark (or the end character if there is no closing quotemark) - currentToken.location?.range.end.character + (currentToken.text.endsWith('"') ? -1 : 0) + util.getLocation(currentToken)?.range.end.character + (currentToken.text.endsWith('"') ? -1 : 0) ), pkgPath ), @@ -655,7 +655,7 @@ export class CompletionsProcessor { const nextNonComment = file.getNextTokenByPredicate(currentToken, (t: Token) => !AllowedTriviaTokens.includes(t.kind), 1); const firstComment = nextNonComment?.leadingTrivia?.find(t => t.kind === TokenKind.Comment); - if (firstComment && util.comparePosition(position, firstComment?.location?.range.start) >= 0) { + if (firstComment && util.comparePosition(position, util.getLocation(firstComment)?.range.start) >= 0) { return true; } return false; diff --git a/src/bscPlugin/definition/DefinitionProvider.ts b/src/bscPlugin/definition/DefinitionProvider.ts index 2b0fc0643..cab22e32e 100644 --- a/src/bscPlugin/definition/DefinitionProvider.ts +++ b/src/bscPlugin/definition/DefinitionProvider.ts @@ -227,7 +227,7 @@ export class DefinitionProvider { const constant = scope?.getConstFileLink(fullName, containingNamespace); if (constant) { this.event.definitions.push( - constant.item.tokens.name?.location + util.getLocation(constant.item.tokens.name) ); return; } @@ -236,14 +236,14 @@ export class DefinitionProvider { const enumLink = scope.getEnumFileLink(fullName, containingNamespace); if (enumLink) { this.event.definitions.push( - enumLink.item.tokens.name.location + util.getLocation(enumLink.item.tokens.name) ); return; } const enumMemberLink = scope.getEnumMemberFileLink(fullName, containingNamespace); if (enumMemberLink) { this.event.definitions.push( - enumMemberLink.item.tokens.name.location + util.getLocation(enumMemberLink.item.tokens.name) ); return; } @@ -251,7 +251,7 @@ export class DefinitionProvider { const interfaceFileLink = scope.getInterfaceFileLink(fullName, containingNamespace); if (interfaceFileLink) { this.event.definitions.push( - interfaceFileLink.item.tokens.name.location + util.getLocation(interfaceFileLink.item.tokens.name) ); return; } @@ -259,7 +259,7 @@ export class DefinitionProvider { const classFileLink = scope.getClassFileLink(fullName, containingNamespace); if (classFileLink) { this.event.definitions.push( - classFileLink.item.tokens.name.location + util.getLocation(classFileLink.item.tokens.name) ); return; } @@ -268,7 +268,7 @@ export class DefinitionProvider { let textToSearchFor = token.text.toLowerCase(); - const previousToken = file.getTokenAt({ line: token.location?.range.start.line, character: token.location?.range.start.character }); + const previousToken = file.getTokenAt({ line: util.getLocation(token)?.range.start.line, character: util.getLocation(token)?.range.start.character }); if (previousToken?.kind === TokenKind.Callfunc) { for (const scope of this.event.program.getScopes()) { @@ -282,7 +282,7 @@ export class DefinitionProvider { const callable = scope.getAllCallables().find((c) => c.callable.name.toLowerCase() === textToSearchFor); // eslint-disable-line @typescript-eslint/no-loop-func if (callable) { this.event.definitions.push( - util.createLocationFromRange(util.pathToUri((callable.callable.file as BrsFile).srcPath), callable.callable.functionStatement.tokens.name.location?.range) + util.createLocationFromRange(util.pathToUri((callable.callable.file as BrsFile).srcPath), util.getLocation(callable.callable.functionStatement.tokens.name)?.range) ); } } @@ -294,12 +294,12 @@ export class DefinitionProvider { // eslint-disable-next-line @typescript-eslint/dot-notation let classToken = file['getTokenBefore'](token, TokenKind.Class); if (classToken) { - let cs = file.parser.ast.findChild((klass) => isClassStatement(klass) && klass.tokens.class.location?.range === classToken.location?.range); + let cs = file.parser.ast.findChild((klass) => isClassStatement(klass) && util.getLocation(klass.tokens.class)?.range === util.getLocation(classToken)?.range); if (cs?.parentClassName) { const nameParts = cs.parentClassName.getNameParts(); let extendedClass = file.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.')); if (extendedClass) { - this.event.definitions.push(util.createLocationFromRange(util.pathToUri(extendedClass.file.srcPath), extendedClass.item.location?.range)); + this.event.definitions.push(util.createLocationFromRange(util.pathToUri(extendedClass.file.srcPath), util.getLocation(extendedClass.item)?.range)); } } return; @@ -327,10 +327,10 @@ export class DefinitionProvider { pathValue, file.pkgPath, util.createRange( - token.location.range.start.line, - token.location.range.start.character + 1, - token.location.range.end.line, - token.location.range.end.character - 1 + util.getLocation(token).range.start.line, + util.getLocation(token).range.start.character + 1, + util.getLocation(token).range.end.line, + util.getLocation(token).range.end.character - 1 ) ); if (link) { @@ -391,7 +391,7 @@ export class DefinitionProvider { FunctionStatement: (statement: FunctionStatement) => { if (statement.getName(file.parseMode).toLowerCase() === textToSearchFor) { const uri = util.pathToUri(file.srcPath); - this.event.definitions.push(util.createLocationFromRange(uri, statement.location?.range)); + this.event.definitions.push(util.createLocationFromRange(uri, util.getLocation(statement)?.range)); } } }), { @@ -418,7 +418,7 @@ export class DefinitionProvider { const namespaceItemStatementHandler = (statement: ClassStatement | FunctionStatement) => { if (!location && statement.tokens.name.text.toLowerCase() === endName) { const uri = util.pathToUri(file.srcPath); - location = util.createLocationFromRange(uri, statement.location?.range); + location = util.createLocationFromRange(uri, util.getLocation(statement)?.range); } }; diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts index a65cda2a0..13463e39e 100644 --- a/src/bscPlugin/hover/HoverProcessor.ts +++ b/src/bscPlugin/hover/HoverProcessor.ts @@ -70,7 +70,7 @@ export class HoverProcessor { } private getConstHover(token: Token, file: BrsFile, scope: Scope, expression: Expression) { - let containingNamespace = file.getNamespaceStatementForPosition(expression?.location?.range?.start)?.getName(ParseMode.BrighterScript); + let containingNamespace = file.getNamespaceStatementForPosition(util.getLocation(expression)?.range?.start)?.getName(ParseMode.BrighterScript); const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); //find a constant with this name @@ -233,7 +233,7 @@ export class HoverProcessor { } } return { - range: token?.location.range, + range: util.getLocation(token).range, contents: hoverContents }; } diff --git a/src/bscPlugin/inlayHints/InlayHintProcessor.ts b/src/bscPlugin/inlayHints/InlayHintProcessor.ts index d947886da..fcb95c8cf 100644 --- a/src/bscPlugin/inlayHints/InlayHintProcessor.ts +++ b/src/bscPlugin/inlayHints/InlayHintProcessor.ts @@ -37,13 +37,13 @@ export class InlayHintProcessor { file.ast.walk(createVisitor({ CallExpression: (call) => { - if (!call.location?.range || !util.rangesIntersectOrTouch(call.location.range, range)) { + if (!util.getLocation(call)?.range || !util.rangesIntersectOrTouch(util.getLocation(call).range, range)) { return; } this.emitParameterNameHints(file, call); }, CallfuncExpression: (call) => { - if (!call.location?.range || !util.rangesIntersectOrTouch(call.location.range, range)) { + if (!util.getLocation(call)?.range || !util.rangesIntersectOrTouch(util.getLocation(call).range, range)) { return; } this.emitParameterNameHintsForCallfunc(file, call); @@ -191,7 +191,7 @@ export class InlayHintProcessor { const arg = args[i]; const param = params[i]; const paramName = param?.tokens?.name?.text; - const argRange = arg?.location?.range; + const argRange = util.getLocation(arg)?.range; if (!paramName || !argRange) { continue; } diff --git a/src/bscPlugin/references/ReferencesProvider.ts b/src/bscPlugin/references/ReferencesProvider.ts index 0cea7f564..829f72d5a 100644 --- a/src/bscPlugin/references/ReferencesProvider.ts +++ b/src/bscPlugin/references/ReferencesProvider.ts @@ -49,12 +49,12 @@ export class ReferencesProvider { file.ast.walk(createVisitor({ AssignmentStatement: (s) => { if (s.tokens.name?.text?.toLowerCase() === searchFor) { - this.event.references.push(util.createLocationFromRange(util.pathToUri(file.srcPath), s.tokens.name.location?.range)); + this.event.references.push(util.createLocationFromRange(util.pathToUri(file.srcPath), util.getLocation(s.tokens.name)?.range)); } }, VariableExpression: (e) => { if (e.tokens.name.text.toLowerCase() === searchFor) { - this.event.references.push(util.createLocationFromRange(util.pathToUri(file.srcPath), e.location?.range)); + this.event.references.push(util.createLocationFromRange(util.pathToUri(file.srcPath), util.getLocation(e)?.range)); } } }), { diff --git a/src/bscPlugin/selectionRanges/SelectionRangesProcessor.ts b/src/bscPlugin/selectionRanges/SelectionRangesProcessor.ts index a7d6b8755..119d32f7b 100644 --- a/src/bscPlugin/selectionRanges/SelectionRangesProcessor.ts +++ b/src/bscPlugin/selectionRanges/SelectionRangesProcessor.ts @@ -3,6 +3,7 @@ import type { Range, Position } from 'vscode-languageserver-protocol'; import { isBrsFile } from '../../astUtils/reflection'; import type { ProvideSelectionRangesEvent } from '../../interfaces'; import type { AstNode } from '../../parser/AstNode'; +import { util } from '../../util'; export class SelectionRangesProcessor { public constructor( @@ -30,7 +31,7 @@ export class SelectionRangesProcessor { // Find the deepest AST node containing this position const innerNode = file.ast.findChildAtPosition(position); - const innerNodeRange = innerNode?.location?.range; + const innerNodeRange = util.getLocation(innerNode)?.range; if (!innerNodeRange) { return undefined; } @@ -43,7 +44,7 @@ export class SelectionRangesProcessor { // the JS/TS smart-select behaviour — we look up the exact lexer token at the // cursor position and use its range as the initial step. const token = file.getTokenAt(position); - const tokenRange = token?.location?.range; + const tokenRange = util.getLocation(token)?.range; if (tokenRange && !rangesEqual(tokenRange, innerNodeRange)) { ranges.push(tokenRange); } @@ -53,7 +54,7 @@ export class SelectionRangesProcessor { // only child statement, or an ExpressionStatement === its expression). let node: AstNode | undefined = innerNode; while (node) { - const nodeRange = node.location?.range; + const nodeRange = util.getLocation(node)?.range; if (nodeRange && !rangesEqual(nodeRange, ranges[ranges.length - 1])) { ranges.push(nodeRange); } diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts index 52a55a920..761e18a90 100644 --- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts +++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.ts @@ -98,8 +98,8 @@ export class BrsFileSemanticTokensProcessor { private addToken(locatable: Locatable, type: SemanticTokenTypes, modifiers: SemanticTokenModifiers[] = []) { //only keep a single token per range. Last-in wins - this.result.set(util.rangeToString(locatable.location.range), { - range: locatable.location.range, + this.result.set(util.rangeToString(util.getLocation(locatable).range), { + range: util.getLocation(locatable).range, tokenType: type, tokenModifiers: modifiers }); diff --git a/src/bscPlugin/symbols/symbolUtils.ts b/src/bscPlugin/symbols/symbolUtils.ts index d1c1b740f..0bc4696c3 100644 --- a/src/bscPlugin/symbols/symbolUtils.ts +++ b/src/bscPlugin/symbols/symbolUtils.ts @@ -98,57 +98,57 @@ function getSymbolsFromAstNode(node: AstNode): SymbolInfo[] { node.walk(createVisitor({ FunctionStatement: (statement) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Function, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Function, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, ClassStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Class, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Class, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, FieldStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Field, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Field, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, MethodStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Method, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Method, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, InterfaceStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Interface, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Interface, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, InterfaceFieldStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Field, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Field, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, InterfaceMethodStatement: (statement, parent) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Method, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Method, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, ConstStatement: (statement) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Constant, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Constant, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, NamespaceStatement: (statement) => { if (statement.nameExpression) { - addSymbol(statement, statement.getNameParts().pop().text, SymbolKind.Namespace, statement.location?.range, statement.nameExpression.location?.range); + addSymbol(statement, statement.getNameParts().pop().text, SymbolKind.Namespace, util.getLocation(statement)?.range, util.getLocation(statement.nameExpression)?.range); } }, EnumStatement: (statement) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.Enum, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.Enum, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } }, EnumMemberStatement: (statement) => { if (statement.tokens.name?.text) { - addSymbol(statement, statement.tokens.name.text, SymbolKind.EnumMember, statement.location?.range, statement.tokens.name.location?.range); + addSymbol(statement, statement.tokens.name.text, SymbolKind.EnumMember, util.getLocation(statement)?.range, util.getLocation(statement.tokens.name)?.range); } } }), { diff --git a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts index 3fa92812d..f9c7fc8c0 100644 --- a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts @@ -80,9 +80,9 @@ export class BrsFilePreTranspileProcessor { if (isAssignmentStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), thenBranch: createBlock({ statements: [ createAssignmentStatement({ @@ -92,7 +92,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), elseBranch: createBlock({ statements: [ createAssignmentStatement({ @@ -102,13 +102,13 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) }); } else if (isDottedSetStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), thenBranch: createBlock({ statements: [ createDottedSetStatement({ @@ -119,7 +119,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), elseBranch: createBlock({ statements: [ createDottedSetStatement({ @@ -130,15 +130,15 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) }); //if this is an indexedSetStatement, and the ternary expression is NOT an index } else if (isIndexedSetStatement(parent) && !parent.indexes?.includes(ternaryExpression)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), thenBranch: createBlock({ statements: [ createIndexedSetStatement({ @@ -151,7 +151,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), elseBranch: createBlock({ statements: [ createIndexedSetStatement({ @@ -164,13 +164,13 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) }); } else if (isAugmentedAssignmentStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark.location), + if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark.location), + then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), thenBranch: createBlock({ statements: [ new AugmentedAssignmentStatement({ @@ -180,7 +180,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark.location), + else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), elseBranch: createBlock({ statements: [ new AugmentedAssignmentStatement({ @@ -190,7 +190,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark.location) + endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) }); } @@ -347,7 +347,7 @@ export class BrsFilePreTranspileProcessor { // skip any changes in an Alias Statement return; } - let containingNamespaceStmt = this.event.file.getNamespaceStatementForPosition(expression.location?.range.start); + let containingNamespaceStmt = this.event.file.getNamespaceStatementForPosition(util.getLocation(expression)?.range.start); let containingNamespace = containingNamespaceStmt?.getName(ParseMode.BrighterScript); const parts = util.splitExpression(expression); diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 643a718da..1427a9e9b 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -74,24 +74,24 @@ export class BrsFileValidator { if (node.args.length > 5) { this.event.program.diagnostics.register({ ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length), - location: node.tokens.methodName.location + location: util.getLocation(node.tokens.methodName) }); } }, DottedGetExpression: (node) => { if (node.tokens.dot?.kind === TokenKind.QuestionDot) { - this.validateMinFirmwareVersionForOptionalChaining(node.tokens.dot?.location?.range); + this.validateMinFirmwareVersionForOptionalChaining(util.getLocation(node.tokens.dot)?.range); } }, IndexedGetExpression: (node) => { if (node.tokens.questionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare) { - const range = node.tokens.questionDot?.location?.range ?? node.tokens.openingSquare?.location?.range; + const range = util.getLocation(node.tokens.questionDot)?.range ?? util.getLocation(node.tokens.openingSquare)?.range; this.validateMinFirmwareVersionForOptionalChaining(range); } }, CallExpression: (node) => { if (node.tokens.openingParen?.kind === TokenKind.QuestionLeftParen) { - this.validateMinFirmwareVersionForOptionalChaining(node.tokens.openingParen?.location?.range); + this.validateMinFirmwareVersionForOptionalChaining(util.getLocation(node.tokens.openingParen)?.range); } this.validateGlobalCallableAvailability(node); }, @@ -117,7 +117,7 @@ export class BrsFileValidator { for (const constructor of node.getConditionalCompileConstructors()) { this.event.program.diagnostics.register({ ...DiagnosticMessages.classConstructorNotAllowedInConditionalCompile(), - location: constructor.tokens.name.location + location: util.getLocation(constructor.tokens.name) }); } @@ -297,7 +297,7 @@ export class BrsFileValidator { } else { this.event.program.diagnostics.register({ ...DiagnosticMessages.expectedExceptionVarToFollowCatch(), - location: node.exceptionVariableExpression?.location ?? node.tokens.catch?.location + location: util.getLocation(node.exceptionVariableExpression) ?? util.getLocation(node.tokens.catch) }); } }, @@ -319,7 +319,7 @@ export class BrsFileValidator { if (node.value) { this.event.program.diagnostics.register({ ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.tokens.functionType?.text), - location: node.location + location: util.getLocation(node) }); } @@ -328,7 +328,7 @@ export class BrsFileValidator { if (!node.value) { this.event.program.diagnostics.register({ ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.tokens.functionType?.text), - location: node.location + location: util.getLocation(node) }); } } @@ -360,7 +360,7 @@ export class BrsFileValidator { ConditionalCompileErrorStatement: (node) => { this.event.program.diagnostics.register({ ...DiagnosticMessages.hashError(node.tokens.message.text), - location: node.location + location: util.getLocation(node) }); }, AliasStatement: (node) => { @@ -388,7 +388,7 @@ export class BrsFileValidator { blockSymbolTable.isOrdered = true; } if (!isFunctionExpression(node.parent) && node.parent) { - node.symbolTable.name = `Block-${node.parent.kind}@${node.location?.range?.start?.line}`; + node.symbolTable.name = `Block-${node.parent.kind}@${util.getLocation(node)?.range?.start?.line}`; // we're a block inside another block (or body). This block is a pocket in the bigger block node.parent.getSymbolTable().addPocketTable({ index: node.parent.statementIndex, @@ -422,7 +422,7 @@ export class BrsFileValidator { ) { this.event.program.diagnostics.register({ ...DiagnosticMessages.reservedBuiltinUsedAsValue(name), - location: node.tokens.name.location + location: util.getLocation(node.tokens.name) }); } }, @@ -506,7 +506,7 @@ export class BrsFileValidator { //the statement was defined in the wrong place. Flag it. this.event.program.diagnostics.register({ ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword), - location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location + location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : util.getLocation(statement) }); } @@ -539,7 +539,7 @@ export class BrsFileValidator { if (name && name.length > BrsFileValidator.MaxFunctionNameLength) { this.event.program.diagnostics.register({ ...DiagnosticMessages.functionNameTooLong(name, name.length, BrsFileValidator.MaxFunctionNameLength), - location: node.tokens.name?.location ?? node.location + location: util.getLocation(node.tokens.name) ?? util.getLocation(node) }); } } @@ -550,7 +550,7 @@ export class BrsFileValidator { for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) { this.event.program.diagnostics.register({ ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments), - location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location + location: util.getLocation(func.parameters[i]?.tokens.name) ?? util.getLocation(func.parameters[i]) ?? util.getLocation(func) }); } } @@ -570,7 +570,7 @@ export class BrsFileValidator { if (memberNames.has(memberNameLower)) { this.event.program.diagnostics.register({ ...DiagnosticMessages.duplicateIdentifier(member.name), - location: member.location + location: util.getLocation(member) }); } else { memberNames.add(memberNameLower); @@ -591,7 +591,7 @@ export class BrsFileValidator { memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind; memberValue = member.value; } - const range = (memberValue ?? member)?.location?.range; + const range = util.getLocation((memberValue ?? member))?.range; if ( //is integer enum, has value, that value type is not integer (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) || @@ -638,7 +638,7 @@ export class BrsFileValidator { if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) { this.event.program.diagnostics.register({ ...DiagnosticMessages.hashConstDoesNotExist(), - location: ccConst.location + location: util.getLocation(ccConst) }); return false; } @@ -673,7 +673,7 @@ export class BrsFileValidator { ) { this.event.program.diagnostics.register({ ...DiagnosticMessages.unexpectedStatementOutsideFunction(), - location: statement.location + location: util.getLocation(statement) }); } } @@ -716,17 +716,17 @@ export class BrsFileValidator { if (isLibraryStatement(result)) { this.event.program.diagnostics.register({ ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'), - location: result.location + location: util.getLocation(result) }); } else if (isImportStatement(result)) { this.event.program.diagnostics.register({ ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'), - location: result.location + location: util.getLocation(result) }); } else if (isAliasStatement(result)) { this.event.program.diagnostics.register({ ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'), - location: result.location + location: util.getLocation(result) }); } } @@ -754,7 +754,7 @@ export class BrsFileValidator { if (isBadTypecastObj) { this.event.program.diagnostics.register({ ...DiagnosticMessages.invalidTypecastStatementApplication(resultVarStr, hasFunctionAncestor), - location: typecastStmt.typecastExpression.obj.location + location: util.getLocation(typecastStmt.typecastExpression.obj) }); } @@ -778,7 +778,7 @@ export class BrsFileValidator { if (!isFirst) { this.event.program.diagnostics.register({ ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of block or namespace'), - location: typecastStmt.location + location: util.getLocation(typecastStmt) }); } } @@ -791,7 +791,7 @@ export class BrsFileValidator { const actualLoopType = statement.tokens.loopType; if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) { this.event.program.diagnostics.register({ - location: statement.tokens.loopType.location, + location: util.getLocation(statement.tokens.loopType), ...DiagnosticMessages.expectedToken(expectedLoopType) }); } @@ -813,7 +813,7 @@ export class BrsFileValidator { //flag continue statements found outside of a loop if (!parent) { this.event.program.diagnostics.register({ - location: statement.location, + location: util.getLocation(statement), ...DiagnosticMessages.illegalContinueStatement() }); } @@ -837,7 +837,7 @@ export class BrsFileValidator { CONTINUE_MIN_FIRMWARE_VERSION, this.event.program.getMinFirmwareVersion() ), - location: statement.location + location: util.getLocation(statement) }); } } @@ -861,11 +861,11 @@ export class BrsFileValidator { //try to highlight the entire left-hand-side expression if possible let range: Range; if (isDottedSetStatement(parent)) { - range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name); + range = util.createBoundingRange(util.getLocation(parent.obj), parent.tokens.dot, parent.tokens.name); } else if (isIndexedSetStatement(parent)) { - range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare); + range = util.createBoundingRange(util.getLocation(parent.obj), parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare); } else { - range = node.location?.range; + range = util.getLocation(node)?.range; } this.event.program.diagnostics.register({ @@ -941,7 +941,7 @@ export class BrsFileValidator { if (diagnostic) { this.event.program.diagnostics.register({ ...diagnostic, - location: node.callee.location + location: util.getLocation(node.callee) }); } } diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 528c084c7..bedcb211b 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -241,7 +241,7 @@ export class ScopeValidator { expr: assignStmt, name: assignStmt.tokens.name.text, type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }), - nameRange: assignStmt.tokens.name.location?.range + nameRange: util.getLocation(assignStmt.tokens.name)?.range }); }); }, @@ -266,7 +266,7 @@ export class ScopeValidator { expr: forEachStmt, name: forEachStmt.tokens.item.text, type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }), - nameRange: forEachStmt.tokens.item.location?.range + nameRange: util.getLocation(forEachStmt.tokens.item)?.range }); this.validateForEachStatement(file, forEachStmt); }); @@ -277,7 +277,7 @@ export class ScopeValidator { expr: funcParam, name: funcParam.tokens.name.text, type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }), - nameRange: funcParam.tokens.name.location?.range + nameRange: util.getLocation(funcParam.tokens.name)?.range }); }); }, @@ -363,7 +363,7 @@ export class ScopeValidator { if (member.key.tokens.value.kind !== TokenKind.StringLiteral) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.computedAAKeyMustBeStringExpression(), - location: member.key.location + location: util.getLocation(member.key) }); } return; @@ -375,7 +375,7 @@ export class ScopeValidator { if (!parts || parts.length === 0) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(), - location: member.key.location + location: util.getLocation(member.key) }); return; } @@ -388,7 +388,7 @@ export class ScopeValidator { if (!value?.startsWith('"')) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.computedAAKeyMustBeStringExpression(), - location: member.key.location + location: util.getLocation(member.key) }); } return; @@ -399,14 +399,14 @@ export class ScopeValidator { if (!this.constResolvesToString(constLink.item.value, enclosingNamespace, scope)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.computedAAKeyMustBeStringExpression(), - location: member.key.location + location: util.getLocation(member.key) }); } return; } this.addMultiScopeDiagnostic({ ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(), - location: member.key.location + location: util.getLocation(member.key) }); } @@ -542,7 +542,7 @@ export class ScopeValidator { const items = chain.slice(cycleStart).map(c => c.fullName).concat(constStatement.fullName); this.addMultiScopeDiagnostic({ ...DiagnosticMessages.circularReferenceDetected(items), - location: constStatement.tokens.name.location + location: util.getLocation(constStatement.tokens.name) }); return; } @@ -608,13 +608,13 @@ export class ScopeValidator { // roSgNode should only ever have 2 args in `createObject` this.addDiagnostic({ ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length), - location: call.location + location: util.getLocation(call) }); } } else if (!platformComponentNames.has(firstParamStringValueLower)) { this.addDiagnostic({ ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue), - location: firstParamToken.location + location: util.getLocation(firstParamToken) }); } else { // This is valid brightscript component @@ -630,7 +630,7 @@ export class ScopeValidator { // Incorrect number of arguments included in `createObject()` this.addDiagnostic({ ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length), - location: call.location + location: util.getLocation(call) }); } @@ -638,7 +638,7 @@ export class ScopeValidator { if (brightScriptComponent?.isDeprecated) { this.addDiagnostic({ ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription), - location: call.location + location: util.getLocation(call) }); } } @@ -655,7 +655,7 @@ export class ScopeValidator { if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) { this.addDiagnostic({ ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName), - location: componentName.location + location: util.getLocation(componentName) }); } } @@ -688,10 +688,10 @@ export class ScopeValidator { const functionFullname = `${callerType.toString()}@.${functionName}`; this.addMultiScopeDiagnostic({ ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()), - location: firstArgToken?.location + location: util.getLocation(firstArgToken) }); } else { - this.validateFunctionCall(file, call, funcType, firstArgToken.location, call.args, 1); + this.validateFunctionCall(file, call, funcType, util.getLocation(firstArgToken), call.args, 1); } } } @@ -704,7 +704,7 @@ export class ScopeValidator { // We're calling a class - get the constructor funcType = funcType.getMemberType('new', getTypeOptions); } - const callErrorLocation = expression?.callee?.location; + const callErrorLocation = util.getLocation(expression?.callee); return this.validateFunctionCall(file, expression.callee, funcType, callErrorLocation, expression.args); } @@ -720,7 +720,7 @@ export class ScopeValidator { return; } const functionFullname = `${callerType.toString()}@.${methodName}`; - const callErrorLocation = expression.location; + const callErrorLocation = util.getLocation(expression); if ((util.isGenericNodeType(callerType, true) && !this.event.program.options.strictCallFunc) || isObjectType(callerType) || isDynamicType(callerType)) { // ignore "general" node return; @@ -852,7 +852,7 @@ export class ScopeValidator { if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData), - location: arg.location + location: util.getLocation(arg) }); } paramIndex++; @@ -896,7 +896,7 @@ export class ScopeValidator { } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData), - location: returnStmt.value?.location ?? returnStmt.location + location: util.getLocation(returnStmt.value) ?? util.getLocation(returnStmt) }); } } @@ -942,7 +942,7 @@ export class ScopeValidator { if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData), - location: dottedSetStmt.location + location: util.getLocation(dottedSetStmt) }); } } @@ -966,7 +966,7 @@ export class ScopeValidator { } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData), - location: assignStmt.location + location: util.getLocation(assignStmt) }); } } @@ -1020,7 +1020,7 @@ export class ScopeValidator { if (!util.binaryOperatorResultType(leftInnerType, binaryExpr.tokens.operator, rightInnerType)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()), - location: binaryExpr.location + location: util.getLocation(binaryExpr) }); return; } @@ -1034,7 +1034,7 @@ export class ScopeValidator { // if the result was dynamic or void, that means there wasn't a valid operation this.addMultiScopeDiagnostic({ ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()), - location: binaryExpr.location + location: util.getLocation(binaryExpr) }); } } @@ -1068,14 +1068,14 @@ export class ScopeValidator { if (!opResult) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()), - location: unaryExpr.location + location: util.getLocation(unaryExpr) }); } } else { // rhs is not a primitive, so no binary operator is allowed this.addMultiScopeDiagnostic({ ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()), - location: unaryExpr.location + location: util.getLocation(unaryExpr) }); } } @@ -1101,7 +1101,7 @@ export class ScopeValidator { // rhs is not a number, so no increment operator is not allowed this.addMultiScopeDiagnostic({ ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()), - location: incStmt.location + location: util.getLocation(incStmt) }); } } @@ -1152,7 +1152,7 @@ export class ScopeValidator { if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemIsDeprecated(), - location: expression.tokens.name.location, + location: util.getLocation(expression.tokens.name), tags: [DiagnosticTag.Deprecated] }); } @@ -1165,14 +1165,14 @@ export class ScopeValidator { if (isUsedAsType) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName), - location: expression.location + location: util.getLocation(expression) }); } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) { if (!isAliasStatement(expression.parent)) { // alias rhs CAN be a type! this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()), - location: expression.location + location: util.getLocation(expression) }); } } else { @@ -1229,7 +1229,7 @@ export class ScopeValidator { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()), - location: expression.location + location: util.getLocation(expression) }); return; } @@ -1243,7 +1243,7 @@ export class ScopeValidator { if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'), - location: expression.location + location: util.getLocation(expression) }); } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) { const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression)); @@ -1251,7 +1251,7 @@ export class ScopeValidator { // there's an enum with this name this.addMultiScopeDiagnostic({ ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'), - location: expression.location + location: util.getLocation(expression) }); } } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) { @@ -1266,7 +1266,7 @@ export class ScopeValidator { message: 'Enum declared here', location: util.createLocationFromRange( util.pathToUri(enumFileLink?.file.srcPath), - enumFileLink?.item?.tokens.name.location?.range + util.getLocation(enumFileLink?.item?.tokens.name)?.range ) }] }); @@ -1322,7 +1322,7 @@ export class ScopeValidator { if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName), - location: expression.location + location: util.getLocation(expression) }); // there's an error... don't worry about the rest of the chain return false; @@ -1339,7 +1339,7 @@ export class ScopeValidator { if (!isSubClassOfDefiningClass) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName), - location: expression.location + location: util.getLocation(expression) }); // there's an error... don't worry about the rest of the chain return false; @@ -1372,7 +1372,7 @@ export class ScopeValidator { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.expressionIsNotConstructable(fullName), - location: newExpression.className.location + location: util.getLocation(newExpression.className) }); } @@ -1388,7 +1388,7 @@ export class ScopeValidator { if (!returns && isStringTypeLike(returnType)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()), - location: func.location + location: util.getLocation(func) }); } } @@ -1619,7 +1619,7 @@ export class ScopeValidator { message: 'Class declared here', location: util.createLocationFromRange( util.pathToUri(classStmtLink.file.srcPath), - classStmtLink?.item.tokens.name.location?.range + util.getLocation(classStmtLink?.item.tokens.name)?.range ) }] }); @@ -1633,7 +1633,7 @@ export class ScopeValidator { if (!IntegerType.instance.isTypeCompatible(assignValueType)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(assignValueType.toString(), 'integer'), - location: assignStmt.location + location: util.getLocation(assignStmt) }); } if (forStmt.increment) { @@ -1641,7 +1641,7 @@ export class ScopeValidator { if (!IntegerType.instance.isTypeCompatible(incrementValueType)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(incrementValueType.toString(), 'integer'), - location: forStmt.increment.location + location: util.getLocation(forStmt.increment) }); } } @@ -1649,7 +1649,7 @@ export class ScopeValidator { if (!IntegerType.instance.isTypeCompatible(finalValueType)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(finalValueType.toString(), 'integer'), - location: forStmt.finalValue.location + location: util.getLocation(forStmt.finalValue) }); } } @@ -1671,7 +1671,7 @@ export class ScopeValidator { // target is not an array nor enumerable this.addMultiScopeDiagnostic({ ...DiagnosticMessages.notIterable(targetType.toString()), - location: forEachStmt.target.location + location: util.getLocation(forEachStmt.target) }); return; } @@ -1686,7 +1686,7 @@ export class ScopeValidator { if (!loopType.isTypeCompatible(targetItemType, data)) { this.addMultiScopeDiagnostic({ ...DiagnosticMessages.assignmentTypeMismatch(targetItemType.toString(), loopType.toString(), data), - location: forEachStmt.typeExpression.location + location: util.getLocation(forEachStmt.typeExpression) }); } } diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 168ec8aab..94fd138bb 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -212,7 +212,7 @@ export class BrsFile implements BscFile { file: this, fullName: loopName, lastPartName: part, - nameRange: namespaceStatement.nameExpression.location?.range, + nameRange: util.getLocation(namespaceStatement.nameExpression)?.range, namespaceStatements: undefined, statements: undefined, classStatements: undefined, @@ -264,7 +264,7 @@ export class BrsFile implements BscFile { //register import statements if (isImportStatement(statement) && statement.tokens.path) { result.push({ - filePathRange: statement.tokens.path.location?.range, + filePathRange: util.getLocation(statement.tokens.path)?.range, destPath: util.getPkgPathFromTarget(this.destPath, statement.filePath), sourceFile: this, text: statement.tokens.path.text @@ -303,7 +303,7 @@ export class BrsFile implements BscFile { */ public getTokenAt(position: Position) { for (let token of this.parser.tokens) { - if (util.rangeContains(token.location?.range, position)) { + if (util.rangeContains(util.getLocation(token)?.range, position)) { return token; } } @@ -314,7 +314,7 @@ export class BrsFile implements BscFile { */ public getCurrentOrNextTokenAt(position: Position) { for (let token of this.parser.tokens) { - if (util.comparePositionToRange(position, token.location?.range) < 0) { + if (util.comparePositionToRange(position, util.getLocation(token)?.range) < 0) { return token; } } @@ -332,7 +332,7 @@ export class BrsFile implements BscFile { this.ast.walk((node) => { const latestContainer = containingNode; //bsc walks depth-first - if (node.location?.range && util.rangeContains(node.location?.range, position)) { + if (util.getLocation(node)?.range && util.rangeContains(util.getLocation(node)?.range, position)) { containingNode = node; } //we had a match before, and don't now. this means we've finished walking down the whole way, and found our match @@ -528,7 +528,7 @@ export class BrsFile implements BscFile { for (let lexerToken of tokens) { for (let triviaToken of lexerToken.leadingTrivia ?? []) { if (triviaToken.kind === TokenKind.Comment) { - processor.tryAdd(triviaToken.text, triviaToken.location?.range); + processor.tryAdd(triviaToken.text, util.getLocation(triviaToken)?.range); } } } @@ -569,8 +569,8 @@ export class BrsFile implements BscFile { //add every parameter for (let param of func.parameters) { scope.variableDeclarations.push({ - nameRange: param.tokens.name.location?.range, - lineIndex: param.tokens.name.location?.range?.start.line, + nameRange: util.getLocation(param.tokens.name)?.range, + lineIndex: util.getLocation(param.tokens.name)?.range?.start.line, name: param.tokens.name.text, getType: () => { return param.getType({ flags: SymbolTypeFlag.typetime }); @@ -582,8 +582,8 @@ export class BrsFile implements BscFile { func.body?.walk(createVisitor({ ForEachStatement: (stmt) => { scope.variableDeclarations.push({ - nameRange: stmt.tokens.item.location?.range, - lineIndex: stmt.tokens.item.location?.range?.start.line, + nameRange: util.getLocation(stmt.tokens.item)?.range, + lineIndex: util.getLocation(stmt.tokens.item)?.range?.start.line, name: stmt.tokens.item.text, getType: () => stmt.getType({ flags: SymbolTypeFlag.runtime }) }); @@ -591,8 +591,8 @@ export class BrsFile implements BscFile { LabelStatement: (stmt) => { const { name: identifier } = stmt.tokens; scope.labelStatements.push({ - nameRange: identifier.location?.range, - lineIndex: identifier.location?.range?.start.line, + nameRange: util.getLocation(identifier)?.range, + lineIndex: util.getLocation(identifier)?.range?.start.line, name: identifier.text }); } @@ -620,8 +620,8 @@ export class BrsFile implements BscFile { if (scope) { const variableName = statement.tokens.name; scope.variableDeclarations.push({ - nameRange: variableName.location?.range, - lineIndex: variableName.location?.range?.start.line, + nameRange: util.getLocation(variableName)?.range, + lineIndex: util.getLocation(variableName)?.range?.start.line, name: variableName.text, getType: () => { return statement.getType({ flags: SymbolTypeFlag.runtime }); @@ -662,10 +662,10 @@ export class BrsFile implements BscFile { callables.push({ isSub: statement.func.tokens.functionType?.text.toLowerCase() === 'sub', name: statement.tokens.name?.text, - nameRange: statement.tokens.name?.location?.range, + nameRange: util.getLocation(statement.tokens.name)?.range, file: this, params: params, - range: statement.func.location?.range, + range: util.getLocation(statement.func)?.range, type: funcType, getName: statement.getName.bind(statement), hasNamespace: !!statement.findAncestor(isNamespaceStatement), @@ -714,9 +714,9 @@ export class BrsFile implements BscFile { return this.cache.getOrAdd(`namespaceStatementForPosition-${position.line}:${position.character}`, () => { let mostSpecificNamespace: NamespaceStatement; for (const statement of this._cachedLookups.namespaceStatements) { - if (util.rangeContains(statement.location?.range, position)) { + if (util.rangeContains(util.getLocation(statement)?.range, position)) { if (mostSpecificNamespace) { - if (util.isRangeInRange(statement.location?.range, mostSpecificNamespace.location?.range)) { + if (util.isRangeInRange(util.getLocation(statement)?.range, util.getLocation(mostSpecificNamespace)?.range)) { mostSpecificNamespace = statement; } } else { @@ -859,11 +859,11 @@ export class BrsFile implements BscFile { let tokens = this.parser.tokens; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; - if (util.rangeContains(token.location?.range, position)) { + if (util.rangeContains(util.getLocation(token)?.range, position)) { return token; } //if the position less than this token range, then this position touches no token, - if (util.positionIsGreaterThanRange(position, token.location?.range) === false) { + if (util.positionIsGreaterThanRange(position, util.getLocation(token)?.range) === false) { let t = tokens[i - 1]; //return the token or the first token return t ? t : tokens[0]; @@ -914,12 +914,12 @@ export class BrsFile implements BscFile { //get class fields and members const statementHandler = (statement: MethodStatement) => { if (statement.getName(file.parseMode).toLowerCase() === textToSearchFor) { - results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), statement.location?.range)); + results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), util.getLocation(statement)?.range)); } }; const fieldStatementHandler = (statement: FieldStatement) => { if (statement.tokens.name.text.toLowerCase() === textToSearchFor) { - results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), statement.location?.range)); + results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), util.getLocation(statement)?.range)); } }; file.parser.ast.walk(createVisitor({ @@ -1006,7 +1006,7 @@ export class BrsFile implements BscFile { const astTranspile = this.ast.transpile(state); const trailingComments = []; if (util.hasLeadingComments(this.parser.eofToken)) { - if (util.isLeadingCommentOnSameLine(this.ast.statements[this.ast.statements.length - 1]?.location, this.parser.eofToken)) { + if (util.isLeadingCommentOnSameLine(util.getLocation(this.ast.statements[this.ast.statements.length - 1]), this.parser.eofToken)) { trailingComments.push(' '); } else { trailingComments.push('\n'); @@ -1361,7 +1361,7 @@ export class BrsFile implements BscFile { fullNameLower: lowerLoopName, parentNameLower: parentNameLower, nameParts: nameParts.slice(0, i), - nameRange: namespaceStatement.nameExpression.location?.range, + nameRange: util.getLocation(namespaceStatement.nameExpression)?.range, lastPartName: part.text, lastPartNameLower: lowerPartName, namespaceStatements: [], diff --git a/src/interfaces.ts b/src/interfaces.ts index fca0f5792..04f8d35fa 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -11,7 +11,7 @@ import type { AstNode, Expression } from './parser/AstNode'; import type { TranspileState } from './parser/TranspileState'; import type { SourceNode } from 'source-map'; import type { BscType } from './types/BscType'; -import type { Token } from './lexer/Token'; +import type { Locatable, Token } from './lexer/Token'; import type { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver'; import type { SymbolTypeFlag } from './SymbolTypeFlag'; import type { Editor } from './astUtils/Editor'; @@ -21,6 +21,7 @@ import type { LazyFileData } from './files/LazyFileData'; import { TokenKind } from './lexer/TokenKind'; import type { BscTypeKind } from './types/BscTypeKind'; import { createToken } from './astUtils/creators'; +import { util } from './util'; import type { SourceFixAllCodeAction } from './CodeActionUtil'; import type { Availability } from './RokuConstants'; @@ -1286,7 +1287,10 @@ export class TypeChainEntry { name: string; type: BscType; data: ExtraSymbolData; - location?: Location; + /** + * The item to get the location from. Falls back to `astNode` if this has no location + */ + locatable?: Locatable; separatorToken?: Token; astNode: AstNode; }) { @@ -1294,20 +1298,20 @@ export class TypeChainEntry { // make a copy of this data this.data = { ...options.data }; this.type = options.type; - this._location = options.location; + this.locatable = options.locatable; this.separatorToken = options.separatorToken ?? createToken(TokenKind.Dot); this.astNode = options.astNode; this.isResolved = this.type?.isResolvable(); } get location(): Location { - return this._location ?? this.astNode?.location; + return util.getLocation(this.locatable) ?? util.getLocation(this.astNode); } public readonly name: string; public readonly type: BscType; public readonly data: ExtraSymbolData; - private readonly _location: Location; + private readonly locatable: Locatable; public readonly separatorToken: Token; public isResolved: boolean; public astNode: AstNode; diff --git a/src/lsp/Project.ts b/src/lsp/Project.ts index d2f51f78d..0295e467c 100644 --- a/src/lsp/Project.ts +++ b/src/lsp/Project.ts @@ -481,7 +481,7 @@ export class Project implements LspProject { } result.push({ uri: util.pathToUri(file.srcPath), - range: importStatement.tokens.path.location.range, + range: util.getLocation(importStatement.tokens.path).range, newText: newText }); } diff --git a/src/parser/BrightScriptDocParser.ts b/src/parser/BrightScriptDocParser.ts index cde2e4843..49aefe062 100644 --- a/src/parser/BrightScriptDocParser.ts +++ b/src/parser/BrightScriptDocParser.ts @@ -64,7 +64,7 @@ export class BrightScriptDocParser { lastTag.detail = lastTag.detail.trim(); } if (haveMatchingTokens) { - lastTag.location = util.createBoundingLocation(lastTag.location, blockLines[blockLines.length - 1].token.location); + lastTag.location = util.createBoundingLocation(lastTag.location, util.getLocation(blockLines[blockLines.length - 1].token)); } } blockLines.length = 0; @@ -99,7 +99,7 @@ export class BrightScriptDocParser { } public getTypeLocationFromToken(token: Token): Location { - if (!token?.location) { + if (!util.getLocation(token)) { return undefined; } const startCurly = token.text.indexOf('{'); @@ -108,15 +108,15 @@ export class BrightScriptDocParser { return undefined; } return { - uri: token.location.uri, + uri: util.getLocation(token).uri, range: { start: { - line: token.location.range.start.line, - character: token.location.range.start.character + startCurly + 1 + line: util.getLocation(token).range.start.line, + character: util.getLocation(token).range.start.character + startCurly + 1 }, end: { - line: token.location.range.start.line, - character: token.location.range.start.character + endCurly + line: util.getLocation(token).range.start.line, + character: util.getLocation(token).range.start.character + endCurly } } }; @@ -151,7 +151,7 @@ export class BrightScriptDocParser { return { ...result, token: token, - location: token?.location + location: util.getLocation(token) }; } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 64de258a4..05f21f0df 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -743,7 +743,7 @@ export class DottedGetExpression extends Expression { name: this.tokens.name?.text, type: result, data: options.data, - location: this.tokens.name?.location ?? this.location, + locatable: this.tokens.name, astNode: this })); if (result || @@ -1764,8 +1764,8 @@ export class SourceLiteralExpression extends Expression { private getClosestLineNumber() { let node: AstNode = this; while (node) { - if (node.location?.range) { - return node.location.range.start.line + 1; + if (util.getLocation(node)?.range) { + return util.getLocation(node).range.start.line + 1; } node = node.parent; } diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index d1c6572b2..bac8df67b 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -1,4 +1,4 @@ -import type { Token, Identifier } from '../lexer/Token'; +import type { Token, Identifier, Locatable } from '../lexer/Token'; import { isToken } from '../lexer/Token'; import type { BlockTerminator, PrintSeparatorToken } from '../lexer/TokenKind'; import { Lexer } from '../lexer/Lexer'; @@ -300,7 +300,7 @@ export class Parser { if (this.options.mode !== ParseMode.BrighterScript) { let diagnostic = { ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName), - location: this.peek().location + location: util.getLocation(this.peek()) }; this.diagnostics.push(diagnostic); } @@ -431,7 +431,7 @@ export class Parser { if (!ignoreDiagnostics) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(asToken.text), - location: asToken.location + location: util.getLocation(asToken) }); } //consume the statement separator @@ -440,7 +440,7 @@ export class Parser { if (!ignoreDiagnostics) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(asToken.text), - location: asToken.location + location: util.getLocation(asToken) }); } } else { @@ -467,7 +467,7 @@ export class Parser { if (params.length >= CallExpression.MaximumArguments) { this.diagnostics.push({ ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments), - location: this.peek().location + location: util.getLocation(this.peek()) }); } @@ -514,7 +514,7 @@ export class Parser { if (this.checkEndOfStatement()) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(extendsToken.text), - location: extendsToken.location + location: util.getLocation(extendsToken) }); } else { parentInterfaceName = this.typeExpression(); @@ -669,7 +669,7 @@ export class Parser { if (this.checkEndOfStatement()) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(extendsKeyword.text), - location: extendsKeyword.location + location: util.getLocation(extendsKeyword) }); } else { parentClassName = this.typeExpression(); @@ -700,7 +700,7 @@ export class Parser { if (endingKeyword.kind !== TokenKind.EndClass) { this.diagnostics.push({ ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'), - location: endingKeyword.location + location: util.getLocation(endingKeyword) }); } @@ -753,7 +753,7 @@ export class Parser { if (overrideKeyword && funcDeclaration.tokens.name.text.toLowerCase() === 'new') { this.diagnostics.push({ ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(), - location: overrideKeyword.location + location: util.getLocation(overrideKeyword) }); } @@ -773,7 +773,7 @@ export class Parser { if (overrideKeyword) { this.diagnostics.push({ ...DiagnosticMessages.classFieldCannotBeOverridden(), - location: overrideKeyword.location + location: util.getLocation(overrideKeyword) }); } @@ -830,7 +830,7 @@ export class Parser { //found something that is neither a class member nor a conditional compile terminator (i.e. `end class`) this.diagnostics.push({ ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -913,7 +913,7 @@ export class Parser { } else { this.diagnostics.push({ ...DiagnosticMessages.missingCallableKeyword(), - location: this.peek().location + location: util.getLocation(this.peek()) }); //TODO we should probably eliminate this entirely, since it's not present in the source code functionType = { @@ -954,7 +954,7 @@ export class Parser { //don't throw this error; let the parser continue this.diagnostics.push({ ...DiagnosticMessages.invalidIdentifier(name.text, lastChar), - location: name.location + location: util.getLocation(name) }); } @@ -962,7 +962,7 @@ export class Parser { if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) { this.diagnostics.push({ ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text), - location: name.location + location: util.getLocation(name) }); } } @@ -987,7 +987,7 @@ export class Parser { if (haveFoundOptional && !param.defaultValue) { this.diagnostics.push({ ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text), - location: param.location + location: util.getLocation(param) }); } @@ -1010,7 +1010,7 @@ export class Parser { if (endFunctionType.kind !== expectedEndKind) { this.diagnostics.push({ ...DiagnosticMessages.closingKeywordMismatch(functionTypeText, endFunctionType.text), - location: endFunctionType.location + location: util.getLocation(endFunctionType) }); } @@ -1046,7 +1046,7 @@ export class Parser { if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) { this.diagnostics.push({ ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -1059,7 +1059,7 @@ export class Parser { if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) { this.diagnostics.push({ ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text), - location: name.location + location: util.getLocation(name) }); } @@ -1092,7 +1092,7 @@ export class Parser { if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) { this.diagnostics.push({ ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text), - location: name.location + location: util.getLocation(name) }); } let asToken: Token; @@ -1349,13 +1349,13 @@ export class Parser { //emit a targeted diagnostic and consume the `next` so the rest of the file parses cleanly. this.diagnostics.push({ ...DiagnosticMessages.mismatchedEndingToken(['end while'], 'next'), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); endWhile = this.advance(); } else { this.diagnostics.push({ ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); if (!whileBlock) { throw this.lastDiagnosticAsError(); @@ -1427,13 +1427,13 @@ export class Parser { //recover: a stray `end while` is a common mistake when the user means `end for`. this.diagnostics.push({ ...DiagnosticMessages.mismatchedEndingToken(['end for', 'next'], 'end while'), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); endForToken = this.advance(); } else { this.diagnostics.push({ ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forToken.text), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); if (!body) { throw this.lastDiagnosticAsError(); @@ -1472,7 +1472,7 @@ export class Parser { } else { this.diagnostics.push({ ...DiagnosticMessages.expectedToken(TokenKind.In), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -1482,7 +1482,7 @@ export class Parser { if (!target) { this.diagnostics.push({ ...DiagnosticMessages.expectedExpressionAfterForEachIn(), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -1497,13 +1497,13 @@ export class Parser { //recover: a stray `end while` is a common mistake when the user means `end for`. this.diagnostics.push({ ...DiagnosticMessages.mismatchedEndingToken(['end for', 'next'], 'end while'), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); endFor = this.advance(); } else { this.diagnostics.push({ ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -1540,7 +1540,7 @@ export class Parser { //the `end namespace` keyword is missing. add a diagnostic, but keep parsing this.diagnostics.push({ ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'), - location: keyword.location + location: util.getLocation(keyword) }); } @@ -1610,7 +1610,7 @@ export class Parser { let token = this.advance(); this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(token.text), - location: token.location + location: util.getLocation(token) }); } } @@ -1684,7 +1684,7 @@ export class Parser { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier('typecast'), location: { - uri: typecastToken.location.uri, + uri: util.getLocation(typecastToken).uri, range: util.createBoundingRange(typecastToken, this.peek()) } }); @@ -1864,7 +1864,7 @@ export class Parser { this.diagnostics.push({ ...DiagnosticMessages.unterminatedTemplateExpression(), location: { - uri: openingBacktick.location.uri, + uri: util.getLocation(openingBacktick).uri, range: util.createBoundingRange(openingBacktick, this.peek()) } }); @@ -1883,7 +1883,7 @@ export class Parser { this.diagnostics.push({ ...DiagnosticMessages.unterminatedTemplateString(), location: { - uri: openingBacktick.location.uri, + uri: util.getLocation(openingBacktick).uri, range: util.createBoundingRange(openingBacktick, this.peek()) } }); @@ -1923,7 +1923,7 @@ export class Parser { if (peek.kind !== TokenKind.Catch) { this.diagnostics.push({ ...DiagnosticMessages.expectedCatchBlockInTryCatch(), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); } else { const catchToken = this.advance(); @@ -1948,7 +1948,7 @@ export class Parser { if (this.peek().kind !== TokenKind.EndTry) { this.diagnostics.push({ ...DiagnosticMessages.expectedTerminator('end try', 'try-catch'), - location: this.peek().location + location: util.getLocation(this.peek()) }); } else { endTryToken = this.advance(); @@ -1970,7 +1970,7 @@ export class Parser { if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) { this.diagnostics.push({ ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(), - location: throwToken.location + location: util.getLocation(throwToken) }); } else { expression = this.expression(); @@ -2008,7 +2008,7 @@ export class Parser { if (expressions.length === 0) { this.diagnostics.push({ ...DiagnosticMessages.missingExpressionsInDimStatement(), - location: this.peek().location + location: util.getLocation(this.peek()) }); } let rightSquareBracket = this.tryConsume(DiagnosticMessages.unmatchedLeftToken('[', 'dim identifier'), TokenKind.RightSquareBracket); @@ -2031,7 +2031,7 @@ export class Parser { if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedColonBeforeIfStatement(), - location: prev.location + location: util.getLocation(prev) }); } } @@ -2066,7 +2066,7 @@ export class Parser { if (!thenBranch) { this.diagnostics.push({ ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } else { @@ -2085,7 +2085,7 @@ export class Parser { if (!elseBranch.isInline) { this.diagnostics.push({ ...DiagnosticMessages.expectedInlineIfStatement(), - location: elseBranch.location + location: util.getLocation(elseBranch) }); } @@ -2093,7 +2093,7 @@ export class Parser { //expecting inline else branch this.diagnostics.push({ ...DiagnosticMessages.expectedInlineIfStatement(), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } else { @@ -2108,7 +2108,7 @@ export class Parser { //missing `else` branch this.diagnostics.push({ ...DiagnosticMessages.expectedStatement('else', 'statement'), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -2126,7 +2126,7 @@ export class Parser { //newline is required this.diagnostics.push({ ...DiagnosticMessages.expectedFinalNewline(), - location: this.peek().location + location: util.getLocation(this.peek()) }); } } @@ -2163,7 +2163,7 @@ export class Parser { //missing endif this.diagnostics.push({ ...DiagnosticMessages.expectedTerminator('end if', 'if'), - location: ifToken.location + location: util.getLocation(ifToken) }); } } @@ -2199,7 +2199,7 @@ export class Parser { //this whole if statement is bogus...add error to the if token and hard-fail this.diagnostics.push({ ...DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'), - location: ifToken.location + location: util.getLocation(ifToken) }); throw this.lastDiagnosticAsError(); } @@ -2223,7 +2223,7 @@ export class Parser { if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) { this.diagnostics.push({ ...DiagnosticMessages.invalidHashIfValue(), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); } @@ -2278,7 +2278,7 @@ export class Parser { //missing #endif this.diagnostics.push({ ...DiagnosticMessages.expectedTerminator('#end if', '#if'), - location: hashIfToken.location + location: util.getLocation(hashIfToken) }); } } @@ -2312,7 +2312,7 @@ export class Parser { //this whole if statement is bogus...add error to the if token and hard-fail this.diagnostics.push({ ...DiagnosticMessages.expectedTerminator(['#end if', '#else if', '#else'], 'conditional compilation', 'block'), - location: hashIfToken.location + location: util.getLocation(hashIfToken) }); throw this.lastDiagnosticAsError(); } @@ -2388,7 +2388,7 @@ export class Parser { ) { this.diagnostics.push({ ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text), - location: peek.location + location: util.getLocation(peek) }); throw this.lastDiagnosticAsError(); } else { @@ -2407,7 +2407,7 @@ export class Parser { if (ReservedWords.has(constName?.text.toLowerCase())) { this.diagnostics.push({ ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text), - location: constName?.location + location: util.getLocation(constName) }); this.lastDiagnosticAsError(); @@ -2419,7 +2419,7 @@ export class Parser { if (assignment.tokens.as || assignment.typeExpression) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)), - location: assignment.tokens.as?.location ?? assignment.typeExpression?.location + location: util.getLocation(assignment.tokens.as) ?? util.getLocation(assignment.typeExpression) }); this.lastDiagnosticAsError(); } @@ -2430,7 +2430,7 @@ export class Parser { } else { this.diagnostics.push({ ...DiagnosticMessages.invalidHashConstValue(), - location: assignment.value.location + location: util.getLocation(assignment.value) }); this.lastDiagnosticAsError(); } @@ -2441,7 +2441,7 @@ export class Parser { if (!this.check(TokenKind.Newline)) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -2461,7 +2461,7 @@ export class Parser { if (!this.check(TokenKind.Newline)) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -2473,7 +2473,7 @@ export class Parser { if (!silent) { this.diagnostics.push({ ...DiagnosticMessages.expectedNewlineOrColon(), - location: this.peek().location + location: util.getLocation(this.peek()) }); } return false; @@ -2487,7 +2487,7 @@ export class Parser { if (isIfStatement(stat) && !stat.isInline) { this.diagnostics.push({ ...DiagnosticMessages.expectedInlineIfStatement(), - location: stat.location + location: util.getLocation(stat) }); } } @@ -2524,7 +2524,7 @@ export class Parser { const colon = this.previous(); this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(colon.text), - location: colon.location + location: util.getLocation(colon) }); } } @@ -2540,13 +2540,13 @@ export class Parser { if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedOperator(), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } else if (isCallExpression(expr)) { this.diagnostics.push({ ...DiagnosticMessages.unexpectedOperator(), - location: expressionStart.location + location: util.getLocation(expressionStart) }); throw this.lastDiagnosticAsError(); } @@ -2595,7 +2595,7 @@ export class Parser { //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment this.diagnostics.push({ ...DiagnosticMessages.expectedStatement(), - location: expressionStart.location + location: util.getLocation(expressionStart) }); return new ExpressionStatement({ expression: expr }); } @@ -2662,7 +2662,7 @@ export class Parser { //print statements can be empty, so look for empty print conditions if (!values.length) { - const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek()); + const endOfStatementLocation = util.setBounds({} as Locatable, printKeyword, this.peek()); let emptyStringLiteral = createStringLiteral('', endOfStatementLocation); values.push(emptyStringLiteral); } @@ -2842,7 +2842,7 @@ export class Parser { for (const annotation of this.pendingAnnotations) { this.diagnostics.push({ ...DiagnosticMessages.unusedAnnotation(), - location: annotation.location + location: util.getLocation(annotation) }); } } @@ -3051,7 +3051,7 @@ export class Parser { if (!leftParen) { // new expression without a following call expression // wrap the name in an expression - const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek()); + const endOfStatementLocation = util.setBounds({} as Locatable, newToken, this.peek()); const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation); return new ExpressionStatement({ expression: exprStmt }); } @@ -3158,7 +3158,7 @@ export class Parser { if (args.length >= CallExpression.MaximumArguments) { this.diagnostics.push({ ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -3353,7 +3353,7 @@ export class Parser { if (params.length >= CallExpression.MaximumArguments) { this.diagnostics.push({ ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments), - location: this.peek().location + location: util.getLocation(this.peek()) }); } @@ -3398,7 +3398,7 @@ export class Parser { if (!this.check(TokenKind.RightCurlyBrace)) { this.diagnostics.push({ ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -3437,7 +3437,7 @@ export class Parser { if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) { this.diagnostics.push({ ...DiagnosticMessages.expectedIdentifier(this.peek().text), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -3528,7 +3528,7 @@ export class Parser { } else { this.diagnostics.push({ ...DiagnosticMessages.unexpectedToken(this.peek().text), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -3600,7 +3600,7 @@ export class Parser { } else { this.diagnostics.push({ ...DiagnosticMessages.unexpectedAAKey(), - location: this.peek().location + location: util.getLocation(this.peek()) }); throw this.lastDiagnosticAsError(); } @@ -3750,7 +3750,7 @@ export class Parser { } this.diagnostics.push({ ...diagnostic, - location: this.peek()?.location + location: util.getLocation(this.peek()) }); } @@ -3774,7 +3774,7 @@ export class Parser { if (!optional && !consumed) { this.diagnostics.push({ ...DiagnosticMessages.expectedNewlineOrColon(), - location: this.peek()?.location + location: util.getLocation(this.peek()) }); } return consumed; diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index fea4837c5..60941f195 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -103,7 +103,7 @@ export class Body extends Statement implements TypedefProvider { //this is the first statement. do nothing related to spacing and newlines //if comment is on same line as prior sibling - } else if (util.hasLeadingComments(statement) && previousStatement && util.getLeadingComments(statement)?.[0]?.location?.range?.start.line === previousStatement.location?.range?.end.line) { + } else if (util.hasLeadingComments(statement) && previousStatement && util.getLocation(util.getLeadingComments(statement)?.[0])?.range?.start.line === util.getLocation(previousStatement)?.range?.end.line) { result.push( ' ' ); @@ -215,7 +215,7 @@ export class AssignmentStatement extends Statement { // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types - options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: variableType, data: options.data, location: this.tokens.name?.location, astNode: this })); + options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: variableType, data: options.data, locatable: this.tokens.name, astNode: this })); return variableType; } @@ -443,7 +443,7 @@ export class Block extends Statement { //is not a comment //if comment is on same line as parent if (util.isLeadingCommentOnSameLine(state.lineage[0]?.location, statement) || - util.isLeadingCommentOnSameLine(previousStatement?.location, statement) + util.isLeadingCommentOnSameLine(util.getLocation(previousStatement), statement) ) { results.push(' '); @@ -1766,7 +1766,7 @@ export class DottedSetStatement extends Statement { options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name?.text, type: result, data: options.data, - location: this.tokens.name?.location, + locatable: this.tokens.name, astNode: this })); return result; diff --git a/src/util.ts b/src/util.ts index 0ad4db9e5..c82fc8473 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1286,7 +1286,7 @@ export class Util { * Gets the bounding range of a bunch of ranges or objects that have ranges * TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this */ - public createBoundingLocation(...locatables: Array<{ location?: Location } | Location | { range?: Range } | Range | undefined>): Location | undefined { + public createBoundingLocation(...locatables: Array): Location | undefined { let uri: string | undefined; let startPosition: Position | undefined; let endPosition: Position | undefined; @@ -1295,6 +1295,12 @@ export class Util { let range: Range; if (!locatable) { continue; + } else if ('source' in locatable) { + const location = this.getLocation(locatable); + range = location?.range; + if (!uri) { + uri = location?.uri; + } } else if ('location' in locatable) { range = locatable.location?.range; if (!uri) { @@ -2253,7 +2259,7 @@ export class Util { case AstNodeKind.FunctionParameterExpression: return [(nextPart as FunctionParameterExpression).tokens.name]; case AstNodeKind.GroupingExpression: - parts.push(createIdentifier('()', nextPart.location)); + parts.push(createIdentifier('()', nextPart)); break loop; default: //we found a non-DottedGet expression, so return because this whole operation is invalid. @@ -2859,7 +2865,7 @@ export class Util { public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) { const leadingCommentRange = this.getLeadingComments(input)?.[0]; if (leadingCommentRange) { - return this.linesTouch(line, leadingCommentRange?.location); + return this.linesTouch(line, util.getLocation(leadingCommentRange)); } return false; } @@ -2946,7 +2952,7 @@ export class Util { name: methodName, type: funcType, data: options.data, - location: methodNameToken.location, + locatable: methodNameToken, separatorToken: createToken(TokenKind.Callfunc), astNode: callExpr })); diff --git a/src/validators/ClassValidator.ts b/src/validators/ClassValidator.ts index ed12a52d6..a23a116af 100644 --- a/src/validators/ClassValidator.ts +++ b/src/validators/ClassValidator.ts @@ -13,6 +13,7 @@ import { DynamicType } from '../types/DynamicType'; import type { BscType } from '../types/BscType'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; import type { BscFile } from '../files/BscFile'; +import { util } from '../util'; export class BsClassValidator { private scope: Scope; @@ -56,7 +57,7 @@ export class BsClassValidator { if (expressionNameLower === 'm') { this.diagnostics.push({ ...DiagnosticMessages.classConstructorIllegalUseOfMBeforeSuperCall(), - location: expression.location + location: util.getLocation(expression) }); } if (isCallExpression(parent) && expressionNameLower === 'super') { @@ -74,7 +75,7 @@ export class BsClassValidator { if (!superCall && !(classStatement.file as BrsFile).isTypedef) { this.diagnostics.push({ ...DiagnosticMessages.classConstructorMissingSuperCall(), - location: newMethod.location + location: util.getLocation(newMethod) }); } } @@ -95,7 +96,7 @@ export class BsClassValidator { this.diagnostics.push({ ...DiagnosticMessages.circularReferenceDetected( Array.from(names.values()).concat(className)), - location: cls.tokens.name.location + location: util.getLocation(cls.tokens.name) }); break; } @@ -130,7 +131,7 @@ export class BsClassValidator { if (methods[lowerMemberName] || fields[lowerMemberName]) { this.diagnostics.push({ ...DiagnosticMessages.duplicateIdentifier(memberName.text), - location: memberName.location + location: util.getLocation(memberName) }); } @@ -156,7 +157,7 @@ export class BsClassValidator { childFieldType.toString(), ancestorMemberType.toString() ), - location: member.location + location: util.getLocation(member) }); } @@ -179,7 +180,7 @@ export class BsClassValidator { childFieldType.toString(), ancestorMemberType.toString() ), - location: member.location + location: util.getLocation(member) }); } } @@ -197,7 +198,7 @@ export class BsClassValidator { ...DiagnosticMessages.missingOverrideKeyword( ancestorAndMember.classStatement.getName(ParseMode.BrighterScript) ), - location: member.location + location: util.getLocation(member) }); } @@ -215,7 +216,7 @@ export class BsClassValidator { ancestorAndMember.member.accessModifier?.text || 'public', ancestorAndMember.classStatement.getName(ParseMode.BrighterScript) ), - location: member.location + location: util.getLocation(member) }); } } From a8aaac12b235d5ca4dbb796071e915c9e003b5b4 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 14:22:49 -0500 Subject: [PATCH 03/26] Phase 3: transpile and sourcemaps read offsets via util.getLocation() - `TranspileState` resolves line/character for sourcemaps with `util.getLocation()` (SG tokens still carry a `location`, so both shapes are supported) - `getSource()` reads the uri from `source.uri` - `lineage` and `transpileEndBlockToken` take locatables directly - ternary transpile copies the `?` token's location into synthetic tokens Co-Authored-By: Claude Opus 5.5 --- .../transpile/BrsFileTranspileProcessor.ts | 32 +++++++++---------- src/parser/BrsTranspileState.ts | 6 ++-- src/parser/Expression.ts | 1 - src/parser/Statement.ts | 2 +- src/parser/TranspileState.ts | 26 +++++++++------ 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts index f9c7fc8c0..5e17961be 100644 --- a/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts +++ b/src/bscPlugin/transpile/BrsFileTranspileProcessor.ts @@ -80,9 +80,9 @@ export class BrsFilePreTranspileProcessor { if (isAssignmentStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark), thenBranch: createBlock({ statements: [ createAssignmentStatement({ @@ -92,7 +92,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark), elseBranch: createBlock({ statements: [ createAssignmentStatement({ @@ -102,13 +102,13 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark) }); } else if (isDottedSetStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark), thenBranch: createBlock({ statements: [ createDottedSetStatement({ @@ -119,7 +119,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark), elseBranch: createBlock({ statements: [ createDottedSetStatement({ @@ -130,15 +130,15 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark) }); //if this is an indexedSetStatement, and the ternary expression is NOT an index } else if (isIndexedSetStatement(parent) && !parent.indexes?.includes(ternaryExpression)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark), thenBranch: createBlock({ statements: [ createIndexedSetStatement({ @@ -151,7 +151,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark), elseBranch: createBlock({ statements: [ createIndexedSetStatement({ @@ -164,13 +164,13 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark) }); } else if (isAugmentedAssignmentStatement(parent)) { ifStatement = createIfStatement({ - if: createToken(TokenKind.If, 'if', util.getLocation(ternaryExpression.tokens.questionMark)), + if: createToken(TokenKind.If, 'if', ternaryExpression.tokens.questionMark), condition: ternaryExpression.test, - then: createToken(TokenKind.Then, 'then', util.getLocation(ternaryExpression.tokens.questionMark)), + then: createToken(TokenKind.Then, 'then', ternaryExpression.tokens.questionMark), thenBranch: createBlock({ statements: [ new AugmentedAssignmentStatement({ @@ -180,7 +180,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - else: createToken(TokenKind.Else, 'else', util.getLocation(ternaryExpression.tokens.questionMark)), + else: createToken(TokenKind.Else, 'else', ternaryExpression.tokens.questionMark), elseBranch: createBlock({ statements: [ new AugmentedAssignmentStatement({ @@ -190,7 +190,7 @@ export class BrsFilePreTranspileProcessor { }) ] }), - endIf: createToken(TokenKind.EndIf, 'end if', util.getLocation(ternaryExpression.tokens.questionMark)) + endIf: createToken(TokenKind.EndIf, 'end if', ternaryExpression.tokens.questionMark) }); } diff --git a/src/parser/BrsTranspileState.ts b/src/parser/BrsTranspileState.ts index 990942305..45aaee6ae 100644 --- a/src/parser/BrsTranspileState.ts +++ b/src/parser/BrsTranspileState.ts @@ -1,4 +1,4 @@ -import type { Location } from 'vscode-languageserver'; +import type { Locatable } from '../lexer/Token'; import { Editor } from '../astUtils/Editor'; import type { BrsFile } from '../files/BrsFile'; import type { FirmwareCapabilities } from '../RokuConstants'; @@ -24,9 +24,7 @@ export class BrsTranspileState extends TranspileState { * the tree of parents, with the first index being direct parent, and the last index being the furthest removed ancestor. * Used to assist blocks in knowing when to add a comment statement to the same line as the first line of the parent */ - lineage = [] as Array<{ - location?: Location; - }>; + lineage = [] as Array; /** * Used by ClassMethodStatements to determine information about their enclosing class diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 05f21f0df..dc86087c1 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -3,7 +3,6 @@ import type { Token, Identifier, Locatable, SourceInfo } from '../lexer/Token'; import type { PrintSeparatorToken } from '../lexer/TokenKind'; import { TokenKind } from '../lexer/TokenKind'; import type { Block, NamespaceStatement } from './Statement'; -import type { Location } from 'vscode-languageserver'; import util from '../util'; import type { BrsTranspileState } from './BrsTranspileState'; import { ParseMode } from './Parser'; diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 60941f195..f740e789e 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -442,7 +442,7 @@ export class Block extends Statement { let statement = this.statements[i]; //is not a comment //if comment is on same line as parent - if (util.isLeadingCommentOnSameLine(state.lineage[0]?.location, statement) || + if (util.isLeadingCommentOnSameLine(state.lineage[0], statement) || util.isLeadingCommentOnSameLine(util.getLocation(previousStatement), statement) ) { results.push(' '); diff --git a/src/parser/TranspileState.ts b/src/parser/TranspileState.ts index 1157ab43c..fd31ba14e 100644 --- a/src/parser/TranspileState.ts +++ b/src/parser/TranspileState.ts @@ -2,12 +2,18 @@ import { SourceNode } from 'source-map'; import type { Location } from 'vscode-languageserver'; import type { BsConfig } from '../BsConfig'; import { TokenKind } from '../lexer/TokenKind'; -import { type Token } from '../lexer/Token'; +import type { Locatable, SourceInfo, Token } from '../lexer/Token'; import type { RangeLike } from '../util'; import { util } from '../util'; import type { TranspileResult } from '../interfaces'; interface TranspileToken { + pos?: number; + end?: number; + source?: SourceInfo; + /** + * Location for tokens that aren't `Locatable` (i.e. SGTokens) + */ location?: Location; text: string; kind?: TokenKind; @@ -63,7 +69,7 @@ export class TranspileState { public newline = '\n'; private getSource(locatable: RangeLike) { - let srcPath = (locatable as { location: Location })?.location?.uri ?? (locatable as Location).uri; + let srcPath = (locatable as Locatable)?.source?.uri ?? (locatable as { location: Location })?.location?.uri ?? (locatable as Location).uri; if (srcPath) { srcPath = util.uriToPath(srcPath); //if a sourceRoot is specified, use that instead of the rootDir @@ -100,11 +106,12 @@ export class TranspileState { * only to then be copied again for the SourceNode constructor */ public tokenToSourceNode(token: TranspileToken) { + const range = (util.getLocation(token as Locatable) ?? token.location)?.range; return new SourceNode( //convert 0-based range line to 1-based SourceNode line - token.location?.range ? token.location.range.start.line + 1 : null, + range ? range.start.line + 1 : null, //range and SourceNode character are both 0-based, so no conversion necessary - token.location?.range ? token.location.range.start.character : null, + range ? range.start.character : null, this.getSource(token), token.text ); @@ -186,11 +193,12 @@ export class TranspileState { return [new SourceNode(null, null, null, [...leadingCommentsSourceNodes, commentIfCommentedOut, defaultValue])]; } - if (!token?.location?.range) { + const range = (util.getLocation(token as Locatable) ?? token?.location)?.range; + if (!range) { return [new SourceNode(null, null, null, [...leadingCommentsSourceNodes, commentIfCommentedOut, token.text])]; } //split multi-line text - if (token.location.range.end.line > token.location.range.start.line) { + if (range.end.line > range.start.line) { const lines = token.text.split(/\r?\n/g); const code = [ this.sourceNode(token, [...leadingCommentsSourceNodes, commentIfCommentedOut, lines[0]]) @@ -201,7 +209,7 @@ export class TranspileState { commentIfCommentedOut, new SourceNode( //convert 0-based range line to 1-based SourceNode line - token.location.range.start.line + i + 1, + range.start.line + i + 1, //SourceNode column is 0-based, and this starts at the beginning of the line 0, this.getSource(token), @@ -215,12 +223,12 @@ export class TranspileState { } } - public transpileEndBlockToken(previousLocatable: { location?: Location }, endToken: Token, defaultValue: string, alwaysAddNewlineBeforeEndToken = true) { + public transpileEndBlockToken(previousLocatable: RangeLike, endToken: Token, defaultValue: string, alwaysAddNewlineBeforeEndToken = true) { const result = []; if (util.hasLeadingComments(endToken)) { // add comments before `end token` - they should be indented - if (util.isLeadingCommentOnSameLine(previousLocatable?.location, endToken)) { + if (util.isLeadingCommentOnSameLine(previousLocatable, endToken)) { this.blockDepth++; result.push(' '); } else { From 5bbd839cd87f0932245dfafb480e75313323d458 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 14:31:05 -0500 Subject: [PATCH 04/26] Phase 4: update tests for flattened locations - read token/node locations through `util.getLocation()` - hand-built parser test tokens use a new `testLocatable()` helper (fixed-width test source) - clone tests skip `pos`/`end`/`source` the same way they used to skip `range` (`source` is intentionally shared between an original and its clone) Co-Authored-By: Claude Opus 5.5 --- src/CommentFlagProcessor.spec.ts | 10 +++--- src/astUtils/reflection.spec.ts | 4 ++- src/files/BrsFile.spec.ts | 10 +++--- src/lexer/Lexer.spec.ts | 16 ++++----- src/parser/AstNode.spec.ts | 4 +-- src/parser/BrightScriptDocParser.spec.ts | 7 ++-- src/parser/Parser.spec.ts | 4 +-- src/parser/tests/Parser.spec.ts | 7 ++-- src/parser/tests/controlFlow/For.spec.ts | 19 +++++----- src/parser/tests/controlFlow/ForEach.spec.ts | 15 ++++---- src/parser/tests/controlFlow/If.spec.ts | 8 ++--- src/parser/tests/controlFlow/While.spec.ts | 11 +++--- src/parser/tests/expression/Additive.spec.ts | 20 +++++------ .../tests/expression/ArrayLiterals.spec.ts | 32 ++++++++--------- .../AssociativeArrayLiterals.spec.ts | 32 ++++++++--------- src/parser/tests/expression/Boolean.spec.ts | 16 ++++----- src/parser/tests/expression/Call.spec.ts | 26 +++++++------- src/parser/tests/expression/Function.spec.ts | 20 +++++------ src/parser/tests/expression/Indexing.spec.ts | 30 ++++++++-------- .../tests/expression/PrefixUnary.spec.ts | 14 ++++---- src/parser/tests/expression/Primary.spec.ts | 36 +++++++++---------- .../TemplateStringExpression.spec.ts | 6 ++-- .../tests/statement/ConstStatement.spec.ts | 2 +- .../tests/statement/Declaration.spec.ts | 12 +++---- src/parser/tests/statement/Dim.spec.ts | 3 +- src/parser/tests/statement/Increment.spec.ts | 10 +++--- .../tests/statement/PrintStatement.spec.ts | 10 +++--- .../tests/statement/ReturnStatement.spec.ts | 9 ++--- src/parser/tests/statement/Set.spec.ts | 29 +++++++-------- src/testHelpers.spec.ts | 13 +++++++ src/util.spec.ts | 12 +++---- 31 files changed, 233 insertions(+), 214 deletions(-) diff --git a/src/CommentFlagProcessor.spec.ts b/src/CommentFlagProcessor.spec.ts index b68e28987..92b4d3698 100644 --- a/src/CommentFlagProcessor.spec.ts +++ b/src/CommentFlagProcessor.spec.ts @@ -122,7 +122,7 @@ describe('CommentFlagProcessor', () => { it('tokenizes bs:disable comment with codes', () => { const token = Lexer.scan(`'bs:disable:1 2 3`).tokens[0].leadingTrivia[0]; expect( - processor['tokenize'](token.text, token.location.range) + processor['tokenize'](token.text, util.getLocation(token).range) ).to.eql({ commentTokenText: `'`, directive: 'disable', @@ -143,7 +143,7 @@ describe('CommentFlagProcessor', () => { const tokens = Lexer.scan(`print "hi" 'bs:disable-line: 123456 999999 aaaab`).tokens; const token = tokens[2].leadingTrivia[1]; expect( - processor['tokenize'](token.text, token.location.range) + processor['tokenize'](token.text, util.getLocation(token).range) ).to.eql({ commentTokenText: `'`, directive: 'line', @@ -163,7 +163,7 @@ describe('CommentFlagProcessor', () => { it('tokenizes bs:disable-line comment with codes', () => { const token = Lexer.scan(`'bs:disable-line:1 2 3`).tokens[0].leadingTrivia[0]; expect( - processor['tokenize'](token.text, token.location?.range) + processor['tokenize'](token.text, util.getLocation(token)?.range) ).to.eql({ commentTokenText: `'`, directive: 'line', @@ -184,7 +184,7 @@ describe('CommentFlagProcessor', () => { const tokens = Lexer.scan(`' bs:disable-line:1`).tokens; const token = tokens[0].leadingTrivia[0]; expect( - processor['tokenize'](token.text, token.location?.range) + processor['tokenize'](token.text, util.getLocation(token)?.range) ).to.eql({ commentTokenText: `'`, directive: 'line', @@ -199,7 +199,7 @@ describe('CommentFlagProcessor', () => { const tokens = Lexer.scan(`'\tbs:disable-line:1`).tokens; const token = tokens[0].leadingTrivia[0]; expect( - processor['tokenize'](token.text, token.location?.range) + processor['tokenize'](token.text, util.getLocation(token)?.range) ).to.eql({ commentTokenText: `'`, directive: 'line', diff --git a/src/astUtils/reflection.spec.ts b/src/astUtils/reflection.spec.ts index 21a0d3548..af1569618 100644 --- a/src/astUtils/reflection.spec.ts +++ b/src/astUtils/reflection.spec.ts @@ -189,8 +189,10 @@ describe('reflection', () => { const charCode: Token & { charCode: number } = { kind: TokenKind.EscapedCharCodeLiteral, text: '0', - location: undefined, isReserved: false, + pos: undefined, + end: undefined, + source: undefined, charCode: 0, leadingTrivia: [] }; diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index db76558f7..e1a3aa771 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -3970,7 +3970,7 @@ describe('BrsFile', () => { //remove newlines and EOF //set each token to a different file for (const token of file.parser.tokens) { - token.location.uri = util.pathToUri(s`${rootDir}/source/file${i++}.bs`); + util.getLocation(token).uri = util.pathToUri(s`${rootDir}/source/file${i++}.bs`); } const result = await program.getTranspiledFileContents(file.srcPath); @@ -3981,8 +3981,8 @@ describe('BrsFile', () => { let sourcemapResult = tokens.map(token => { let originalPosition = consumer.originalPositionFor({ //convert token 0-based line to source-map 1-based line for the lookup - line: token.location?.range.start.line + 1, - column: token.location?.range.start.character + line: util.getLocation(token)?.range.start.line + 1, + column: util.getLocation(token)?.range.start.character }); return { kind: token.kind, @@ -3997,8 +3997,8 @@ describe('BrsFile', () => { expect(sourcemapResult).to.eql( tokens.map(token => ({ kind: token.kind, - start: token.location?.range.start, - source: util.uriToPath(token.location.uri) + start: util.getLocation(token)?.range.start, + source: util.uriToPath(util.getLocation(token).uri) })) ); }); diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index 730886fc3..2fc3ea2d1 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -170,12 +170,12 @@ describe('lexer', () => { it('computes range properly both with and without whitespace', () => { let withoutWhitespace = Lexer.scan(`sub Main()\n bob = true\nend sub`).tokens - .map(x => rangeToArray(x.location?.range)); + .map(x => rangeToArray(util.getLocation(x)?.range)); let withWhitespace = Lexer.scan(`sub Main()\n bob = true\nend sub`).tokens //filter out the whitespace...we only care that it was computed during the scan .filter(x => x.kind !== TokenKind.Whitespace) - .map(x => rangeToArray(x.location?.range)); + .map(x => rangeToArray(util.getLocation(x)?.range)); /*eslint-disable */ let expectedLocations = [ @@ -279,7 +279,7 @@ describe('lexer', () => { end sub `, { includeWhitespace: true - }).tokens.map(x => [...rangeToArray(x.location?.range), x.text]); + }).tokens.map(x => [...rangeToArray(util.getLocation(x)?.range), x.text]); expect(tokens).to.eql([ [0, 0, 0, 1, '\n'], @@ -317,7 +317,7 @@ describe('lexer', () => { //ignore the Eof token .filter(x => x.kind !== TokenKind.Eof); - expect(tokens.map(x => x.location?.range)).to.eql([ + expect(tokens.map(x => util.getLocation(x)?.range)).to.eql([ Range.create(0, 0, 0, 3), // sub Range.create(0, 3, 0, 4), // \n Range.create(1, 0, 1, 3), // sub @@ -968,7 +968,7 @@ describe('lexer', () => { ); expect(tokens.map(x => { return { - range: x.location?.range, + range: util.getLocation(x)?.range, kind: x.kind }; })).to.eql([ @@ -1465,7 +1465,7 @@ describe('lexer', () => { describe('location tracking', () => { it('tracks starting and ending locations including whitespace', () => { let { tokens } = Lexer.scan(`sub foo()\n print "bar"\r\nend sub`, { includeWhitespace: true }); - expect(tokens.map(t => t.location?.range)).to.eql([ + expect(tokens.map(t => util.getLocation(t)?.range)).to.eql([ Range.create(0, 0, 0, 3), // sub Range.create(0, 3, 0, 4), // Range.create(0, 4, 0, 7), // foo @@ -1484,7 +1484,7 @@ describe('lexer', () => { it('tracks starting and ending locations excluding whitespace', () => { let { tokens } = Lexer.scan(`sub foo()\n print "bar"\r\nend sub`, { includeWhitespace: false }); - expect(tokens.map(t => t.location?.range)).to.eql([ + expect(tokens.map(t => util.getLocation(t)?.range)).to.eql([ Range.create(0, 0, 0, 3), // sub Range.create(0, 4, 0, 7), // foo Range.create(0, 7, 0, 8), // ( @@ -1529,7 +1529,7 @@ describe('lexer', () => { ]); //verify the location of `rem` - expect(tokens.map(t => [t.location?.range.start.character, t.location?.range.end.character])).to.eql([ + expect(tokens.map(t => [util.getLocation(t)?.range.start.character, util.getLocation(t)?.range.end.character])).to.eql([ [0, 6], // person [6, 7], // . [7, 10], // rem diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index 691e85ad2..b7f7b9992 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -387,7 +387,7 @@ describe('AstNode', () => { //skip these properties if ( - ['parent', 'symbolTable', 'range'].includes(key) || + ['parent', 'symbolTable', 'pos', 'end', 'source'].includes(key) || //this is a circular reference property or the `returnType` prop, skip it (isFunctionExpression(original) && (key === 'functionStatement' || key === 'returnType')) || //circular reference property for annotations @@ -1912,7 +1912,7 @@ describe('AstNode', () => { */ function getText(code: string, node: AstNode) { const lines = code.split(/\r?\n/); - const { start, end } = node.range; + const { start, end } = util.getLocation(node).range; if (start.line === end.line) { return lines[start.line].slice(start.character, end.character); } diff --git a/src/parser/BrightScriptDocParser.spec.ts b/src/parser/BrightScriptDocParser.spec.ts index a34167673..a7e67676f 100644 --- a/src/parser/BrightScriptDocParser.spec.ts +++ b/src/parser/BrightScriptDocParser.spec.ts @@ -1,14 +1,13 @@ import { expect } from 'chai'; import { brsDocParser } from './BrightScriptDocParser'; import { Parser } from './Parser'; -import { expectTypeToBe } from '../testHelpers.spec'; +import { expectTypeToBe, testLocatable } from '../testHelpers.spec'; import { SymbolTypeFlag } from '../SymbolTypeFlag'; import { IntegerType } from '../types/IntegerType'; import { UnionType } from '../types/UnionType'; import { isReferenceType } from '../astUtils/reflection'; import { createToken } from '../astUtils/creators'; import { TokenKind } from '../lexer/TokenKind'; -import util from '../util'; describe('BrightScriptDocParser', () => { @@ -332,7 +331,7 @@ describe('BrightScriptDocParser', () => { describe('getTypeExpressionFromTypeString', () => { it('should get the location of the type', () => { const text = '\' @param {integer} test'; - const commentToken = createToken(TokenKind.Comment, text, util.createLocation(1, 0, 1, text.length)); + const commentToken = createToken(TokenKind.Comment, text, testLocatable(1, 0, 1, text.length)); const typeLoc = brsDocParser.getTypeLocationFromToken(commentToken); expect(typeLoc.range.start.character).to.equal(10); expect(typeLoc.range.end.character).to.equal(17); @@ -348,7 +347,7 @@ describe('BrightScriptDocParser', () => { '\' @param }integer{ }}' ]; for (const text of texts) { - const commentToken = createToken(TokenKind.Comment, text, util.createLocation(1, 0, 1, text.length)); + const commentToken = createToken(TokenKind.Comment, text, testLocatable(1, 0, 1, text.length)); const typeLoc = brsDocParser.getTypeLocationFromToken(commentToken); expect(typeLoc).to.be.undefined; } diff --git a/src/parser/Parser.spec.ts b/src/parser/Parser.spec.ts index db6ec80e6..7fd30fbb9 100644 --- a/src/parser/Parser.spec.ts +++ b/src/parser/Parser.spec.ts @@ -17,7 +17,7 @@ import { IntegerType } from '../types/IntegerType'; import { FloatType } from '../types/FloatType'; import { StringType } from '../types/StringType'; import { ArrayType, DynamicType, UnionType } from '../types'; -import { standardizePath as s } from '../util'; +import { standardizePath as s, util } from '../util'; import { InlineInterfaceType } from '../types/InlineInterfaceType'; describe('parser', () => { @@ -3618,5 +3618,5 @@ function expectCommentWithText(stat: Statement, text: string) { } export function failStatementType(stat: Statement, type: string) { - assert.fail(`Statement ${stat.constructor.name} line ${stat.location?.range.start.line} is not a ${type}`); + assert.fail(`Statement ${stat.constructor.name} line ${util.getLocation(stat)?.range.start.line} is not a ${type}`); } diff --git a/src/parser/tests/Parser.spec.ts b/src/parser/tests/Parser.spec.ts index e1e7496a2..5b8e9a759 100644 --- a/src/parser/tests/Parser.spec.ts +++ b/src/parser/tests/Parser.spec.ts @@ -11,10 +11,9 @@ export function token(kind: TokenKind, text?: string): Token { kind: kind, text: text!, isReserved: ReservedWords.has((text ?? '').toLowerCase()), - //synthetic test tokens used to default to `null`, but the parser does range arithmetic - //on token.location.range for paths like splitting `exitwhile` into two tokens. Give a - //zero-width location at (0,0) so those paths work without each test having to spell it out. - location: { uri: '', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } }, + pos: undefined, + end: undefined, + source: undefined, leadingTrivia: [] }; } diff --git a/src/parser/tests/controlFlow/For.spec.ts b/src/parser/tests/controlFlow/For.spec.ts index 1d8ab09e8..0e46a5f73 100644 --- a/src/parser/tests/controlFlow/For.spec.ts +++ b/src/parser/tests/controlFlow/For.spec.ts @@ -1,3 +1,4 @@ +import { testLocatable } from '../../../testHelpers.spec'; import { expect } from '../../../chai-config.spec'; import { Parser } from '../../Parser'; import { TokenKind } from '../../../lexer/TokenKind'; @@ -108,49 +109,49 @@ describe('parser for loops', () => { kind: TokenKind.For, text: 'for', isReserved: true, - location: util.createLocation(0, 0, 0, 3), + ...testLocatable(0, 0, 0, 3), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'i', isReserved: false, - location: util.createLocation(0, 4, 0, 5), + ...testLocatable(0, 4, 0, 5), leadingTrivia: [] }, { kind: TokenKind.Equal, text: '=', isReserved: false, - location: util.createLocation(0, 6, 0, 7), + ...testLocatable(0, 6, 0, 7), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '0', isReserved: false, - location: util.createLocation(0, 8, 0, 9), + ...testLocatable(0, 8, 0, 9), leadingTrivia: [] }, { kind: TokenKind.To, text: 'to', isReserved: false, - location: util.createLocation(0, 10, 0, 12), + ...testLocatable(0, 10, 0, 12), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '10', isReserved: false, - location: util.createLocation(0, 13, 0, 15), + ...testLocatable(0, 13, 0, 15), leadingTrivia: [] }, { kind: TokenKind.Newline, text: '\n', isReserved: false, - location: util.createLocation(0, 15, 0, 16), + ...testLocatable(0, 15, 0, 16), leadingTrivia: [] }, // loop body isn't significant for location tracking, so helper functions are safe @@ -163,7 +164,7 @@ describe('parser for loops', () => { kind: TokenKind.EndFor, text: 'end for', isReserved: false, - location: util.createLocation(2, 0, 2, 8), + ...testLocatable(2, 0, 2, 8), leadingTrivia: [] }, EOF @@ -171,7 +172,7 @@ describe('parser for loops', () => { expect(diagnostics).to.be.lengthOf(0); expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location.range).to.deep.include( + expect(util.getLocation(ast.statements[0]).range).to.deep.include( Range.create(0, 0, 2, 8) ); }); diff --git a/src/parser/tests/controlFlow/ForEach.spec.ts b/src/parser/tests/controlFlow/ForEach.spec.ts index c2789fe27..16950d2e5 100644 --- a/src/parser/tests/controlFlow/ForEach.spec.ts +++ b/src/parser/tests/controlFlow/ForEach.spec.ts @@ -1,3 +1,4 @@ +import { testLocatable } from '../../../testHelpers.spec'; import { expect } from '../../../chai-config.spec'; import { ParseMode, Parser } from '../../Parser'; @@ -70,35 +71,35 @@ describe('parser foreach loops', () => { kind: TokenKind.ForEach, text: 'for each', isReserved: true, - location: util.createLocation(0, 0, 0, 8), + ...testLocatable(0, 0, 0, 8), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'a', isReserved: false, - location: util.createLocation(0, 9, 0, 10), + ...testLocatable(0, 9, 0, 10), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'in', isReserved: true, - location: util.createLocation(0, 11, 0, 13), + ...testLocatable(0, 11, 0, 13), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'b', isReserved: false, - location: util.createLocation(0, 14, 0, 15), + ...testLocatable(0, 14, 0, 15), leadingTrivia: [] }, { kind: TokenKind.Newline, text: '\n', isReserved: false, - location: util.createLocation(0, 15, 0, 16), + ...testLocatable(0, 15, 0, 16), leadingTrivia: [] }, // loop body isn't significant for location tracking, so helper functions are safe @@ -111,7 +112,7 @@ describe('parser foreach loops', () => { kind: TokenKind.EndFor, text: 'end for', isReserved: false, - location: util.createLocation(2, 0, 2, 7), + ...testLocatable(2, 0, 2, 7), leadingTrivia: [] }, EOF @@ -119,7 +120,7 @@ describe('parser foreach loops', () => { expect(diagnostics).to.be.lengthOf(0); expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location.range).deep.include( + expect(util.getLocation(ast.statements[0]).range).deep.include( Range.create(0, 0, 2, 7) ); }); diff --git a/src/parser/tests/controlFlow/If.spec.ts b/src/parser/tests/controlFlow/If.spec.ts index ade0345bc..a826490fa 100644 --- a/src/parser/tests/controlFlow/If.spec.ts +++ b/src/parser/tests/controlFlow/If.spec.ts @@ -758,15 +758,15 @@ describe('parser if statements', () => { expect(diagnostics).to.be.lengthOf(0); const then1 = (ast.statements[0] as IfStatement).thenBranch; - expect(then1.location).to.eql(util.createBoundingLocation(...then1.statements)); + expect(util.getLocation(then1)).to.eql(util.createBoundingLocation(...then1.statements)); const then2 = (ast.statements[1] as IfStatement).thenBranch; - expect(then2.location).to.eql(util.createBoundingLocation(...then2.statements)); + expect(util.getLocation(then2)).to.eql(util.createBoundingLocation(...then2.statements)); const else1 = (ast.statements[2] as IfStatement).elseBranch as Block; - expect(else1.location).to.eql(util.createBoundingLocation(...else1.statements)); + expect(util.getLocation(else1)).to.eql(util.createBoundingLocation(...else1.statements)); const else2 = (ast.statements[3] as IfStatement).elseBranch as Block; - expect(else2.location).to.eql(util.createBoundingLocation(...else2.statements)); + expect(util.getLocation(else2)).to.eql(util.createBoundingLocation(...else2.statements)); }); }); diff --git a/src/parser/tests/controlFlow/While.spec.ts b/src/parser/tests/controlFlow/While.spec.ts index e5c5964d4..0f7335993 100644 --- a/src/parser/tests/controlFlow/While.spec.ts +++ b/src/parser/tests/controlFlow/While.spec.ts @@ -1,3 +1,4 @@ +import { testLocatable } from '../../../testHelpers.spec'; import { expect } from '../../../chai-config.spec'; import { Parser } from '../../Parser'; @@ -82,21 +83,21 @@ describe('parser while statements', () => { kind: TokenKind.While, text: 'while', isReserved: true, - location: util.createLocation(0, 0, 0, 5), + ...testLocatable(0, 0, 0, 5), leadingTrivia: [] }, { kind: TokenKind.True, text: 'true', isReserved: true, - location: util.createLocation(0, 6, 0, 10), + ...testLocatable(0, 6, 0, 10), leadingTrivia: [] }, { kind: TokenKind.Newline, text: '\n', isReserved: false, - location: util.createLocation(0, 10, 0, 11), + ...testLocatable(0, 10, 0, 11), leadingTrivia: [] }, // loop body isn't significant for location tracking, so helper functions are safe @@ -109,7 +110,7 @@ describe('parser while statements', () => { kind: TokenKind.EndWhile, text: 'end while', isReserved: false, - location: util.createLocation(2, 0, 2, 9), + ...testLocatable(2, 0, 2, 9), leadingTrivia: [] }, EOF @@ -117,7 +118,7 @@ describe('parser while statements', () => { expect(diagnostics[0]?.message).not.to.exist; expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location.range).deep.include( + expect(util.getLocation(ast.statements[0]).range).deep.include( Range.create(0, 0, 2, 9) ); }); diff --git a/src/parser/tests/expression/Additive.spec.ts b/src/parser/tests/expression/Additive.spec.ts index 59b5604bf..17090bcc1 100644 --- a/src/parser/tests/expression/Additive.spec.ts +++ b/src/parser/tests/expression/Additive.spec.ts @@ -5,7 +5,7 @@ import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { util } from '../../../util'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import type { AssignmentStatement } from '../../Statement'; describe('parser additive expressions', () => { @@ -53,61 +53,61 @@ describe('parser additive expressions', () => { text: '_', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 2) + ...testLocatable(0, 2, 0, 2) }, { kind: TokenKind.IntegerLiteral, text: '1', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 5) + ...testLocatable(0, 4, 0, 5) }, { kind: TokenKind.Plus, text: '+', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 6, 0, 7) + ...testLocatable(0, 6, 0, 7) }, { kind: TokenKind.IntegerLiteral, text: '2', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 8, 0, 9) + ...testLocatable(0, 8, 0, 9) }, { kind: TokenKind.Plus, text: '+', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 10, 0, 11) + ...testLocatable(0, 10, 0, 11) }, { kind: TokenKind.IntegerLiteral, text: '3', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 12, 0, 13) + ...testLocatable(0, 12, 0, 13) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 13, 0, 14) + ...testLocatable(0, 13, 0, 14) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).to.deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).to.deep.include( Range.create(0, 4, 0, 13) ); }); diff --git a/src/parser/tests/expression/ArrayLiterals.spec.ts b/src/parser/tests/expression/ArrayLiterals.spec.ts index 8ef980d81..1e0042a86 100644 --- a/src/parser/tests/expression/ArrayLiterals.spec.ts +++ b/src/parser/tests/expression/ArrayLiterals.spec.ts @@ -4,7 +4,7 @@ import { Parser } from '../../Parser'; import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; -import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import { isArrayLiteralExpression, isAssignmentStatement, isDottedGetExpression, isLiteralExpression } from '../../../astUtils/reflection'; import type { AssignmentStatement } from '../../Statement'; @@ -231,99 +231,99 @@ describe('parser array literals', () => { text: 'a', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.LeftSquareBracket, text: '[', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 5) + ...testLocatable(0, 4, 0, 5) }, { kind: TokenKind.RightSquareBracket, text: ']', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 8, 0, 9) + ...testLocatable(0, 8, 0, 9) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 9, 0, 10) + ...testLocatable(0, 9, 0, 10) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 0, 1, 1) + ...testLocatable(1, 0, 1, 1) }, { kind: TokenKind.Identifier, text: 'b', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 0, 2, 1) + ...testLocatable(2, 0, 2, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 2, 2, 3) + ...testLocatable(2, 2, 2, 3) }, { kind: TokenKind.LeftSquareBracket, text: '[', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 4, 2, 5) + ...testLocatable(2, 4, 2, 5) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(3, 0, 3, 1) + ...testLocatable(3, 0, 3, 1) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(4, 0, 4, 1) + ...testLocatable(4, 0, 4, 1) }, { kind: TokenKind.RightSquareBracket, text: ']', leadingTrivia: [], isReserved: false, - location: util.createLocation(5, 0, 5, 1) + ...testLocatable(5, 0, 5, 1) }, { kind: TokenKind.Eof, text: '', leadingTrivia: [], isReserved: false, - location: util.createLocation(5, 1, 5, 2) + ...testLocatable(5, 1, 5, 2) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).deep.include( Range.create(0, 4, 0, 9) ); - expect((parser.ast.statements[1] as AssignmentStatement).value.location.range).deep.include( + expect(util.getLocation((parser.ast.statements[1] as AssignmentStatement).value).range).deep.include( Range.create(2, 4, 5, 1) ); }); diff --git a/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts b/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts index e951cd82a..783a86ece 100644 --- a/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts +++ b/src/parser/tests/expression/AssociativeArrayLiterals.spec.ts @@ -7,7 +7,7 @@ import { Range } from 'vscode-languageserver'; import type { AssignmentStatement } from '../../Statement'; import type { AAIndexedMemberExpression, AALiteralExpression, AAMemberExpression } from '../../Expression'; import { isAAIndexedMemberExpression, isAALiteralExpression, isAAMemberExpression, isAssignmentStatement, isDottedGetExpression, isLiteralExpression } from '../../../astUtils/reflection'; -import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import { util } from '../../../util'; @@ -341,99 +341,99 @@ describe('parser associative array literals', () => { text: 'a', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.LeftCurlyBrace, text: '{', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 5) + ...testLocatable(0, 4, 0, 5) }, { kind: TokenKind.RightCurlyBrace, text: '}', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 8, 0, 9) + ...testLocatable(0, 8, 0, 9) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 9, 0, 10) + ...testLocatable(0, 9, 0, 10) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 0, 1, 1) + ...testLocatable(1, 0, 1, 1) }, { kind: TokenKind.Identifier, text: 'b', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 0, 2, 1) + ...testLocatable(2, 0, 2, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 2, 2, 3) + ...testLocatable(2, 2, 2, 3) }, { kind: TokenKind.LeftCurlyBrace, text: '{', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 4, 2, 5) + ...testLocatable(2, 4, 2, 5) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(3, 0, 3, 1) + ...testLocatable(3, 0, 3, 1) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(4, 0, 4, 1) + ...testLocatable(4, 0, 4, 1) }, { kind: TokenKind.RightCurlyBrace, text: '}', leadingTrivia: [], isReserved: false, - location: util.createLocation(5, 0, 5, 1) + ...testLocatable(5, 0, 5, 1) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(5, 1, 5, 2) + ...testLocatable(5, 1, 5, 2) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).to.deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).to.deep.include( Range.create(0, 4, 0, 9) ); - expect((parser.ast.statements[1] as AssignmentStatement).value.location.range).to.deep.include( + expect(util.getLocation((parser.ast.statements[1] as AssignmentStatement).value).range).to.deep.include( Range.create(2, 4, 5, 1) ); }); diff --git a/src/parser/tests/expression/Boolean.spec.ts b/src/parser/tests/expression/Boolean.spec.ts index 98e867045..5703fe340 100644 --- a/src/parser/tests/expression/Boolean.spec.ts +++ b/src/parser/tests/expression/Boolean.spec.ts @@ -5,7 +5,7 @@ import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { util } from '../../../util'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import type { AssignmentStatement } from '../../Statement'; describe('parser boolean expressions', () => { @@ -51,47 +51,47 @@ describe('parser boolean expressions', () => { text: 'a', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.True, text: 'true', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 4, 0, 8) + ...testLocatable(0, 4, 0, 8) }, { kind: TokenKind.And, text: 'and', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 9, 0, 12) + ...testLocatable(0, 9, 0, 12) }, { kind: TokenKind.False, text: 'false', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 13, 0, 18) + ...testLocatable(0, 13, 0, 18) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 18, 0, 19) + ...testLocatable(0, 18, 0, 19) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).deep.include( Range.create(0, 4, 0, 18) ); }); diff --git a/src/parser/tests/expression/Call.spec.ts b/src/parser/tests/expression/Call.spec.ts index 06c032062..4f4a23080 100644 --- a/src/parser/tests/expression/Call.spec.ts +++ b/src/parser/tests/expression/Call.spec.ts @@ -7,7 +7,7 @@ import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import type { ExpressionStatement, FunctionStatement } from '../../Statement'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; -import { expectDiagnostics, expectDiagnosticsIncludes } from '../../../testHelpers.spec'; +import { expectDiagnostics, expectDiagnosticsIncludes, testLocatable } from '../../../testHelpers.spec'; import { isAssignmentStatement, isCallExpression, isDottedGetExpression, isDottedSetStatement, isExpressionStatement, isIndexedGetExpression, isReturnStatement } from '../../../astUtils/reflection'; import { util } from '../../../util'; @@ -15,7 +15,7 @@ describe('parser call expressions', () => { it('parses named function calls', () => { const { ast, diagnostics } = Parser.parse([ identifier('RebootSystem'), - { kind: TokenKind.LeftParen, text: '(', location: null as any, leadingTrivia: [] }, + token(TokenKind.LeftParen, '('), token(TokenKind.RightParen, ')'), EOF ]); @@ -66,7 +66,7 @@ describe('parser call expressions', () => { it('allows closing parentheses on separate line in BrighterScript mode', () => { const { ast, diagnostics } = Parser.parse([ identifier('RebootSystem'), - { kind: TokenKind.LeftParen, text: '(', location: null as any, leadingTrivia: [] }, + token(TokenKind.LeftParen, '('), token(TokenKind.Newline, '\\n'), token(TokenKind.Newline, '\\n'), token(TokenKind.RightParen, ')'), @@ -129,9 +129,9 @@ describe('parser call expressions', () => { it('accepts arguments', () => { const { ast, diagnostics } = Parser.parse([ identifier('add'), - { kind: TokenKind.LeftParen, text: '(', location: null as any, leadingTrivia: [] }, + token(TokenKind.LeftParen, '('), token(TokenKind.IntegerLiteral, '1'), - { kind: TokenKind.Comma, text: ',', location: null as any, leadingTrivia: [] }, + token(TokenKind.Comma, ','), token(TokenKind.IntegerLiteral, '2'), token(TokenKind.RightParen, ')'), EOF @@ -155,55 +155,55 @@ describe('parser call expressions', () => { text: 'foo', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 3) + ...testLocatable(0, 0, 0, 3) }, { kind: TokenKind.LeftParen, text: '(', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 3, 0, 4) + ...testLocatable(0, 3, 0, 4) }, { kind: TokenKind.StringLiteral, text: `"bar"`, leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 9) + ...testLocatable(0, 4, 0, 9) }, { kind: TokenKind.Comma, text: ',', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 9, 0, 10) + ...testLocatable(0, 9, 0, 10) }, { kind: TokenKind.StringLiteral, text: `"baz"`, leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 11, 0, 16) + ...testLocatable(0, 11, 0, 16) }, { kind: TokenKind.RightParen, text: ')', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 16, 0, 17) + ...testLocatable(0, 16, 0, 17) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 17, 0, 18) + ...testLocatable(0, 17, 0, 18) } ]); expect(diagnostics).to.be.lengthOf(0); expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location?.range).to.deep.include( + expect(util.getLocation(ast.statements[0])?.range).to.deep.include( Range.create(0, 0, 0, 17) ); }); diff --git a/src/parser/tests/expression/Function.spec.ts b/src/parser/tests/expression/Function.spec.ts index edfe72ebb..2c68e25fe 100644 --- a/src/parser/tests/expression/Function.spec.ts +++ b/src/parser/tests/expression/Function.spec.ts @@ -5,7 +5,7 @@ import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { util } from '../../../util'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import type { AssignmentStatement } from '../../Statement'; describe('parser', () => { @@ -405,61 +405,61 @@ describe('parser', () => { text: '_', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.Sub, text: 'sub', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 4, 0, 7) + ...testLocatable(0, 4, 0, 7) }, { kind: TokenKind.LeftParen, text: '(', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 11, 0, 12) + ...testLocatable(0, 11, 0, 12) }, { kind: TokenKind.RightParen, text: ')', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 12, 0, 13) + ...testLocatable(0, 12, 0, 13) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 13, 0, 14) + ...testLocatable(0, 13, 0, 14) }, { kind: TokenKind.EndSub, text: 'end sub', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 0, 2, 7) + ...testLocatable(2, 0, 2, 7) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 7, 2, 8) + ...testLocatable(2, 7, 2, 8) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).to.deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).to.deep.include( Range.create(0, 4, 2, 7) ); }); diff --git a/src/parser/tests/expression/Indexing.spec.ts b/src/parser/tests/expression/Indexing.spec.ts index fb8d7ec1e..e9f5b269b 100644 --- a/src/parser/tests/expression/Indexing.spec.ts +++ b/src/parser/tests/expression/Indexing.spec.ts @@ -7,7 +7,7 @@ import { Range } from 'vscode-languageserver'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import type { IndexedSetStatement } from '../../Statement'; import { AssignmentStatement } from '../../Statement'; -import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectDiagnostics, expectDiagnosticsIncludes, expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import { isAssignmentStatement, isDottedGetExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isVariableExpression } from '../../../astUtils/reflection'; import type { DottedGetExpression, IndexedGetExpression, VariableExpression } from '../../Expression'; import { WalkMode } from '../../../astUtils/visitors'; @@ -104,96 +104,96 @@ describe('parser indexing', () => { text: 'a', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.Identifier, text: 'foo', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 7) + ...testLocatable(0, 4, 0, 7) }, { kind: TokenKind.Dot, text: '.', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 7, 0, 8) + ...testLocatable(0, 7, 0, 8) }, { kind: TokenKind.Identifier, text: 'bar', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 8, 0, 11) + ...testLocatable(0, 8, 0, 11) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 11, 0, 12) + ...testLocatable(0, 11, 0, 12) }, { kind: TokenKind.Identifier, text: 'b', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 0, 1, 1) + ...testLocatable(1, 0, 1, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 2, 1, 3) + ...testLocatable(1, 2, 1, 3) }, { kind: TokenKind.Identifier, text: 'bar', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 4, 1, 7) + ...testLocatable(1, 4, 1, 7) }, { kind: TokenKind.LeftSquareBracket, text: '[', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 7, 1, 8) + ...testLocatable(1, 7, 1, 8) }, { kind: TokenKind.IntegerLiteral, text: '2', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 8, 1, 9) + ...testLocatable(1, 8, 1, 9) }, { kind: TokenKind.RightSquareBracket, text: ']', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 9, 1, 10) + ...testLocatable(1, 9, 1, 10) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 10, 1, 11) + ...testLocatable(1, 10, 1, 11) } ]); expectZeroDiagnostics(parser); - expect(parser.ast.statements.map(s => (s as AssignmentStatement).value.location.range)).to.deep.equal([ + expect(parser.ast.statements.map(s => util.getLocation((s as AssignmentStatement).value).range)).to.deep.equal([ Range.create(0, 4, 0, 11), Range.create(1, 4, 1, 10) ]); diff --git a/src/parser/tests/expression/PrefixUnary.spec.ts b/src/parser/tests/expression/PrefixUnary.spec.ts index 1b6536f3d..71f820003 100644 --- a/src/parser/tests/expression/PrefixUnary.spec.ts +++ b/src/parser/tests/expression/PrefixUnary.spec.ts @@ -4,7 +4,7 @@ import { Parser } from '../../Parser'; import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import type { AssignmentStatement } from '../../Statement'; import { util } from '../../../util'; @@ -83,40 +83,40 @@ describe('parser prefix unary expressions', () => { text: '_false', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 6) + ...testLocatable(0, 0, 0, 6) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 7, 0, 8) + ...testLocatable(0, 7, 0, 8) }, { kind: TokenKind.Not, text: 'not', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 9, 0, 12) + ...testLocatable(0, 9, 0, 12) }, { kind: TokenKind.True, text: 'true', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 13, 0, 17) + ...testLocatable(0, 13, 0, 17) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 17, 0, 18) + ...testLocatable(0, 17, 0, 18) } ]); expectZeroDiagnostics(parser); - expect((parser.ast.statements[0] as AssignmentStatement).value.location.range).to.deep.include( + expect(util.getLocation((parser.ast.statements[0] as AssignmentStatement).value).range).to.deep.include( Range.create(0, 9, 0, 17) ); }); diff --git a/src/parser/tests/expression/Primary.spec.ts b/src/parser/tests/expression/Primary.spec.ts index 111a75c5f..80759c6be 100644 --- a/src/parser/tests/expression/Primary.spec.ts +++ b/src/parser/tests/expression/Primary.spec.ts @@ -5,7 +5,7 @@ import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { util } from '../../../util'; import type { AssignmentStatement } from '../../Statement'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; describe('parser primary expressions', () => { @@ -68,110 +68,110 @@ describe('parser primary expressions', () => { text: 'a', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 1) + ...testLocatable(0, 0, 0, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 2, 0, 3) + ...testLocatable(0, 2, 0, 3) }, { kind: TokenKind.IntegerLiteral, text: '5', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 5) + ...testLocatable(0, 4, 0, 5) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 5, 0, 6) + ...testLocatable(0, 5, 0, 6) }, { kind: TokenKind.Identifier, text: 'b', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 0, 1, 1) + ...testLocatable(1, 0, 1, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 2, 1, 3) + ...testLocatable(1, 2, 1, 3) }, { kind: TokenKind.StringLiteral, text: `"foo"`, leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 4, 1, 9) + ...testLocatable(1, 4, 1, 9) }, { kind: TokenKind.Newline, text: '\n', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 9, 1, 10) + ...testLocatable(1, 9, 1, 10) }, { kind: TokenKind.Identifier, text: 'c', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 0, 2, 1) + ...testLocatable(2, 0, 2, 1) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 2, 2, 3) + ...testLocatable(2, 2, 2, 3) }, { kind: TokenKind.LeftParen, text: '(', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 4, 2, 5) + ...testLocatable(2, 4, 2, 5) }, { kind: TokenKind.IntegerLiteral, text: '0', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 6, 2, 7) + ...testLocatable(2, 6, 2, 7) }, { kind: TokenKind.RightParen, text: ')', leadingTrivia: [], isReserved: false, - location: util.createLocation(2, 8, 2, 9) + ...testLocatable(2, 8, 2, 9) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(1, 9, 1, 10) //TODO are these numbers right? + ...testLocatable(1, 9, 1, 10) //TODO are these numbers right? } ]); const assignments = parser.ast.statements as AssignmentStatement[]; expectZeroDiagnostics(parser); - expect(assignments[0].value.location.range).to.deep.include( + expect(util.getLocation(assignments[0].value).range).to.deep.include( Range.create(0, 4, 0, 5) ); - expect(assignments[1].value.location.range).to.deep.include( + expect(util.getLocation(assignments[1].value).range).to.deep.include( Range.create(1, 4, 1, 9) ); - expect(assignments[2].value.location.range).to.deep.include( + expect(util.getLocation(assignments[2].value).range).to.deep.include( Range.create(2, 4, 2, 9) ); }); diff --git a/src/parser/tests/expression/TemplateStringExpression.spec.ts b/src/parser/tests/expression/TemplateStringExpression.spec.ts index 7c597ea5a..6bd2ebdfa 100644 --- a/src/parser/tests/expression/TemplateStringExpression.spec.ts +++ b/src/parser/tests/expression/TemplateStringExpression.spec.ts @@ -20,7 +20,7 @@ describe('TemplateStringExpression', () => { it('generates correct locations for quasis', () => { let { tokens } = Lexer.scan('print `0xAAAAAA${"0xBBBBBB"}0xCCCCCC`'); expect( - tokens.filter(x => /"?0x/.test(x.text)).map(x => x.location?.range) + tokens.filter(x => /"?0x/.test(x.text)).map(x => util.getLocation(x)?.range) ).to.eql([ util.createRange(0, 7, 0, 15), // 0xAAAAAA util.createRange(0, 17, 0, 27), // "0xBBBBBB" @@ -34,7 +34,7 @@ describe('TemplateStringExpression', () => { tokens.shift(); expect( //compute the length of the token char spread - tokens.filter(x => x.text !== '').map(x => [x.location.range.end.character - x.location.range.start.character, x.text]) + tokens.filter(x => x.text !== '').map(x => [util.getLocation(x).range.end.character - util.getLocation(x).range.start.character, x.text]) ).to.eql([ '`', '${', @@ -418,6 +418,6 @@ describe('TemplateStringExpression', () => { end function `); const ann = parser.ast.statements[0].annotations![0]; - expect(ann.location?.range).to.eql(util.createRange(1, 12, 3, 14)); + expect(util.getLocation(ann)?.range).to.eql(util.createRange(1, 12, 3, 14)); }); }); diff --git a/src/parser/tests/statement/ConstStatement.spec.ts b/src/parser/tests/statement/ConstStatement.spec.ts index b3a38b1d1..d4cafb577 100644 --- a/src/parser/tests/statement/ConstStatement.spec.ts +++ b/src/parser/tests/statement/ConstStatement.spec.ts @@ -54,7 +54,7 @@ describe('ConstStatement', () => { expect(value).to.be.instanceof(LiteralExpression); expect(value.tokens.value?.text).to.eql('"abc"'); //ensure range is correct - expect(statement.location?.range).to.eql(util.createRange(0, 0, 0, 21)); + expect(util.getLocation(statement)?.range).to.eql(util.createRange(0, 0, 0, 21)); }); it('produces typedef', async () => { diff --git a/src/parser/tests/statement/Declaration.spec.ts b/src/parser/tests/statement/Declaration.spec.ts index 3a5a64cf3..d9a009949 100644 --- a/src/parser/tests/statement/Declaration.spec.ts +++ b/src/parser/tests/statement/Declaration.spec.ts @@ -4,7 +4,7 @@ import { Parser } from '../../Parser'; import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; -import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { expectZeroDiagnostics, testLocatable } from '../../../testHelpers.spec'; import { util } from '../../../util'; describe('parser variable declarations', () => { @@ -92,33 +92,33 @@ describe('parser variable declarations', () => { text: 'foo', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 0, 0, 3) + ...testLocatable(0, 0, 0, 3) }, { kind: TokenKind.Equal, text: '=', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 4, 0, 5) + ...testLocatable(0, 4, 0, 5) }, { kind: TokenKind.Invalid, text: 'invalid', leadingTrivia: [], isReserved: true, - location: util.createLocation(0, 6, 0, 13) + ...testLocatable(0, 6, 0, 13) }, { kind: TokenKind.Eof, text: '\0', leadingTrivia: [], isReserved: false, - location: util.createLocation(0, 13, 0, 14) + ...testLocatable(0, 13, 0, 14) } ]); expectZeroDiagnostics(parser); - expect(parser.ast.statements[0].location?.range).to.deep.include( + expect(util.getLocation(parser.ast.statements[0])?.range).to.deep.include( Range.create(0, 0, 0, 13) ); }); diff --git a/src/parser/tests/statement/Dim.spec.ts b/src/parser/tests/statement/Dim.spec.ts index a1eeb5936..709768095 100644 --- a/src/parser/tests/statement/Dim.spec.ts +++ b/src/parser/tests/statement/Dim.spec.ts @@ -3,6 +3,7 @@ import type { DimStatement } from '../../Statement'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import { Parser } from '../../Parser'; import { expectDiagnostics } from '../../../testHelpers.spec'; +import { util } from '../../../util'; describe('parser DimStatement', () => { it('parses properly', () => { @@ -80,5 +81,5 @@ function validatePass(text: string, dimStatementIndex: number, identifierText: s expect(dimStatement.dimensions).to.exist; expect(dimStatement.dimensions!.length).to.equal(dimensionsCount); expect(dimStatement.tokens.closingSquare).to.exist; - expect(dimStatement.location).to.exist; + expect(util.getLocation(dimStatement)).to.exist; } diff --git a/src/parser/tests/statement/Increment.spec.ts b/src/parser/tests/statement/Increment.spec.ts index 6ff119e19..6e6dcb94d 100644 --- a/src/parser/tests/statement/Increment.spec.ts +++ b/src/parser/tests/statement/Increment.spec.ts @@ -6,7 +6,7 @@ import { EOF, identifier, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { DiagnosticMessages } from '../../../DiagnosticMessages'; import util from '../../../util'; -import { expectDiagnostics } from '../../../testHelpers.spec'; +import { expectDiagnostics, testLocatable } from '../../../testHelpers.spec'; describe('parser postfix unary expressions', () => { it('parses postfix \'++\' for variables', () => { @@ -107,27 +107,27 @@ describe('parser postfix unary expressions', () => { text: 'someNumber', isReserved: false, leadingTrivia: [], - location: util.createLocation(0, 0, 0, 10) + ...testLocatable(0, 0, 0, 10) }, { kind: TokenKind.PlusPlus, text: '++', isReserved: false, leadingTrivia: [], - location: util.createLocation(0, 10, 0, 12) + ...testLocatable(0, 10, 0, 12) }, { kind: TokenKind.Eof, text: '\0', isReserved: false, leadingTrivia: [], - location: util.createLocation(0, 12, 0, 13) + ...testLocatable(0, 12, 0, 13) } ]); expect(diagnostics).to.be.lengthOf(0); expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location?.range).deep.include( + expect(util.getLocation(ast.statements[0])?.range).deep.include( Range.create(0, 0, 0, 12) ); }); diff --git a/src/parser/tests/statement/PrintStatement.spec.ts b/src/parser/tests/statement/PrintStatement.spec.ts index d7b0553db..0dd8e4dc1 100644 --- a/src/parser/tests/statement/PrintStatement.spec.ts +++ b/src/parser/tests/statement/PrintStatement.spec.ts @@ -4,7 +4,7 @@ import { TokenKind } from '../../../lexer/TokenKind'; import { EOF, token } from '../Parser.spec'; import { Range } from 'vscode-languageserver'; import { Program } from '../../../Program'; -import { rootDir } from '../../../testHelpers.spec'; +import { rootDir, testLocatable } from '../../../testHelpers.spec'; import { getTestTranspile } from '../../../testHelpers.spec'; import util from '../../../util'; @@ -80,28 +80,28 @@ describe('parser print statements', () => { kind: TokenKind.Print, text: 'print', isReserved: true, - location: util.createLocation(0, 0, 0, 5), + ...testLocatable(0, 0, 0, 5), leadingTrivia: [] }, { kind: TokenKind.StringLiteral, text: `"foo"`, isReserved: false, - location: util.createLocation(0, 6, 0, 11), + ...testLocatable(0, 6, 0, 11), leadingTrivia: [] }, { kind: TokenKind.Eof, text: '\0', isReserved: false, - location: util.createLocation(0, 11, 0, 12), + ...testLocatable(0, 11, 0, 12), leadingTrivia: [] } ]); expect(diagnostics).to.be.lengthOf(0); expect(ast.statements).to.be.lengthOf(1); - expect(ast.statements[0].location?.range).to.deep.include(Range.create(0, 0, 0, 11)); + expect(util.getLocation(ast.statements[0])?.range).to.deep.include(Range.create(0, 0, 0, 11)); }); describe('transpile', () => { diff --git a/src/parser/tests/statement/ReturnStatement.spec.ts b/src/parser/tests/statement/ReturnStatement.spec.ts index 3ab47e9ca..758e618e2 100644 --- a/src/parser/tests/statement/ReturnStatement.spec.ts +++ b/src/parser/tests/statement/ReturnStatement.spec.ts @@ -1,3 +1,4 @@ +import { testLocatable } from '../../../testHelpers.spec'; import { expect } from '../../../chai-config.spec'; import { Parser } from '../../Parser'; @@ -52,7 +53,7 @@ describe('parser return statements', () => { token(TokenKind.Newline, '\\n'), token(TokenKind.Return, 'return'), identifier('RebootSystem'), - { kind: TokenKind.LeftParen, text: '(', location: null as any, leadingTrivia: [] }, + token(TokenKind.LeftParen, '('), token(TokenKind.RightParen, ')'), token(TokenKind.Newline, '\\n'), token(TokenKind.EndFunction, 'end function'), @@ -82,14 +83,14 @@ describe('parser return statements', () => { kind: TokenKind.Return, text: 'return', isReserved: true, - location: util.createLocation(1, 2, 1, 8), + ...testLocatable(1, 2, 1, 8), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '5', isReserved: false, - location: util.createLocation(1, 9, 1, 10), + ...testLocatable(1, 9, 1, 10), leadingTrivia: [] }, token(TokenKind.Newline, '\\n'), @@ -98,7 +99,7 @@ describe('parser return statements', () => { ]); expect(diagnostics).to.be.lengthOf(0); - expect((ast.statements[0] as FunctionStatement).func.body.statements[0]?.location?.range).to.exist.and.to.deep.include( + expect(util.getLocation((ast.statements[0] as FunctionStatement).func.body.statements[0])?.range).to.exist.and.to.deep.include( Range.create(1, 2, 1, 10) ); }); diff --git a/src/parser/tests/statement/Set.spec.ts b/src/parser/tests/statement/Set.spec.ts index cc7faed1a..639b62e82 100644 --- a/src/parser/tests/statement/Set.spec.ts +++ b/src/parser/tests/statement/Set.spec.ts @@ -1,3 +1,4 @@ +import { testLocatable } from '../../../testHelpers.spec'; import { expect } from '../../../chai-config.spec'; import { Parser } from '../../Parser'; @@ -132,98 +133,98 @@ describe('parser indexed assignment', () => { kind: TokenKind.Identifier, text: 'arr', isReserved: false, - location: util.createLocation(0, 0, 0, 3), + ...testLocatable(0, 0, 0, 3), leadingTrivia: [] }, { kind: TokenKind.LeftSquareBracket, text: '[', isReserved: false, - location: util.createLocation(0, 3, 0, 4), + ...testLocatable(0, 3, 0, 4), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '0', isReserved: false, - location: util.createLocation(0, 4, 0, 5), + ...testLocatable(0, 4, 0, 5), leadingTrivia: [] }, { kind: TokenKind.RightSquareBracket, text: ']', isReserved: false, - location: util.createLocation(0, 5, 0, 6), + ...testLocatable(0, 5, 0, 6), leadingTrivia: [] }, { kind: TokenKind.Equal, text: '=', isReserved: false, - location: util.createLocation(0, 7, 0, 8), + ...testLocatable(0, 7, 0, 8), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '1', isReserved: false, - location: util.createLocation(0, 9, 0, 10), + ...testLocatable(0, 9, 0, 10), leadingTrivia: [] }, { kind: TokenKind.Newline, text: '\n', isReserved: false, - location: util.createLocation(0, 10, 0, 11), + ...testLocatable(0, 10, 0, 11), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'obj', isReserved: false, - location: util.createLocation(1, 0, 1, 3), + ...testLocatable(1, 0, 1, 3), leadingTrivia: [] }, { kind: TokenKind.Dot, text: '.', isReserved: false, - location: util.createLocation(1, 3, 1, 4), + ...testLocatable(1, 3, 1, 4), leadingTrivia: [] }, { kind: TokenKind.Identifier, text: 'a', isReserved: false, - location: util.createLocation(1, 4, 1, 5), + ...testLocatable(1, 4, 1, 5), leadingTrivia: [] }, { kind: TokenKind.Equal, text: '=', isReserved: false, - location: util.createLocation(1, 6, 1, 7), + ...testLocatable(1, 6, 1, 7), leadingTrivia: [] }, { kind: TokenKind.IntegerLiteral, text: '5', isReserved: false, - location: util.createLocation(1, 8, 1, 9), + ...testLocatable(1, 8, 1, 9), leadingTrivia: [] }, { kind: TokenKind.Eof, text: '\0', isReserved: false, - location: util.createLocation(1, 10, 1, 11), + ...testLocatable(1, 10, 1, 11), leadingTrivia: [] } ]); expect(diagnostics).to.be.empty; expect(ast.statements).to.be.lengthOf(2); - expect(ast.statements.map(s => s.location?.range)).to.deep.equal([ + expect(ast.statements.map(s => util.getLocation(s)?.range)).to.deep.equal([ Range.create(0, 0, 0, 10), Range.create(1, 0, 1, 9) ]); diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 836bf28fd..f4aa5676d 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -18,6 +18,7 @@ import type { LspDiagnostic } from './lsp/LspProject'; import { Project } from './lsp/Project'; import { isProgram, isProject } from './astUtils/reflection'; import type { WorkspaceConfig } from './lsp/ProjectManager'; +import type { Locatable, SourceInfo } from './lexer/Token'; export const cwd = s`${__dirname}/../`; export const tempDir = s`${__dirname}/../.tmp`; @@ -32,6 +33,18 @@ export const workspaceSettings: WorkspaceConfig = { }; export const trim = undent; + +/** + * A shared source for hand-built test tokens, where every line is 1000 characters long + */ +const testSource: SourceInfo = { uri: undefined, lineStarts: Array.from({ length: 1000 }, (x, i) => i * 1000) }; + +/** + * Create a `Locatable` for a hand-built test token from line/character positions + */ +export function testLocatable(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Locatable { + return { pos: (startLine * 1000) + startCharacter, end: (endLine * 1000) + endCharacter, source: testSource }; +} const sinon = createSandbox(); beforeEach(() => { diff --git a/src/util.spec.ts b/src/util.spec.ts index 8b0fb5a93..2933c0756 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -6,7 +6,7 @@ import type { BsConfig } from './BsConfig'; import * as fsExtra from 'fs-extra'; import { createSandbox } from 'sinon'; import { DiagnosticMessages } from './DiagnosticMessages'; -import { tempDir, rootDir, expectTypeToBe } from './testHelpers.spec'; +import { tempDir, rootDir, expectTypeToBe, testLocatable } from './testHelpers.spec'; import { TypeChainEntry } from './interfaces'; import { NamespaceType } from './types/NamespaceType'; import { ClassType } from './types/ClassType'; @@ -1390,9 +1390,9 @@ describe('util', () => { describe('processTypeChain', () => { it('should find the correct details in a list of type resolutions', () => { const nodes = [ - createVariableExpression('Alpha', util.createLocation(1, 1, 2, 2)), - createVariableExpression('Beta', util.createLocation(2, 2, 3, 3)), - createVariableExpression('CharlieProp', util.createLocation(3, 3, 4, 4)) + createVariableExpression('Alpha', testLocatable(1, 1, 2, 2)), + createVariableExpression('Beta', testLocatable(2, 2, 3, 3)), + createVariableExpression('CharlieProp', testLocatable(3, 3, 4, 4)) ]; const chain = [ @@ -1411,8 +1411,8 @@ describe('util', () => { it('respects the separatorToken', () => { const nodes = [ - createVariableExpression('Custom', util.createLocation(1, 1, 2, 2)), - createVariableExpression('someCallFunc', util.createLocation(2, 2, 3, 3)) + createVariableExpression('Custom', testLocatable(1, 1, 2, 2)), + createVariableExpression('someCallFunc', testLocatable(2, 2, 3, 3)) ]; const chain = [ new TypeChainEntry({ name: 'roSGNodeCustom', type: new ComponentType('Custom'), data: { flags: SymbolTypeFlag.runtime }, astNode: nodes[0] }), From 8a5613b131462abdaca08e3e7474fd6b6a46f4dd Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 15:27:07 -0500 Subject: [PATCH 05/26] Fix class definition lookup comparing computed ranges by reference `util.getLocation()` returns a new object each call, so comparing ranges with `===` never matched. Compare the tokens instead. Co-Authored-By: Claude Opus 5.5 --- src/bscPlugin/definition/DefinitionProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bscPlugin/definition/DefinitionProvider.ts b/src/bscPlugin/definition/DefinitionProvider.ts index cab22e32e..05ae5dcea 100644 --- a/src/bscPlugin/definition/DefinitionProvider.ts +++ b/src/bscPlugin/definition/DefinitionProvider.ts @@ -294,7 +294,7 @@ export class DefinitionProvider { // eslint-disable-next-line @typescript-eslint/dot-notation let classToken = file['getTokenBefore'](token, TokenKind.Class); if (classToken) { - let cs = file.parser.ast.findChild((klass) => isClassStatement(klass) && util.getLocation(klass.tokens.class)?.range === util.getLocation(classToken)?.range); + let cs = file.parser.ast.findChild((klass) => isClassStatement(klass) && klass.tokens.class === classToken); if (cs?.parentClassName) { const nameParts = cs.parentClassName.getNameParts(); let extendedClass = file.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.')); From c38d15473c1c362acb9e05f62e3f64e167f78d3f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sat, 26 Sep 2026 15:32:25 -0500 Subject: [PATCH 06/26] Add unit tests for offset/line conversion boundaries - `util.getLocation`: LF/CRLF newline tokens, trailing newline + Eof, surrogate pairs, multi-line template strings, split `elseif`/`exitwhile` tokens, multi-line nodes - `util.setBounds`: min/max, mixed sources, no-source items - `findChildAtPosition`: inclusive end, column 0 after a newline, surrogate pairs, LF and CRLF Co-Authored-By: Claude Opus 5.5 --- src/parser/AstNode.spec.ts | 28 +++++++++++ src/util.spec.ts | 95 +++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/parser/AstNode.spec.ts b/src/parser/AstNode.spec.ts index b7f7b9992..11710572a 100644 --- a/src/parser/AstNode.spec.ts +++ b/src/parser/AstNode.spec.ts @@ -217,6 +217,34 @@ describe('AstNode', () => { const foxtrot = file.ast.findChildAtPosition(util.createPosition(3, 71))!; expect(foxtrot.tokens.name.text).to.eql('foxtrot'); }); + + function testPositionBoundaries(newline: string) { + const file = program.setFile('source/main.brs', [ + 'sub main()', + ' alpha = 1', + ' s = "😀" + alpha', + 'end sub' + ].join(newline)); + //start of the literal + expect(file.ast.findChildAtPosition(util.createPosition(1, 12)).tokens.value.text).to.eql('1'); + //just after the end of the literal (range end is inclusive) + expect(file.ast.findChildAtPosition(util.createPosition(1, 13)).tokens.value.text).to.eql('1'); + //column 0 of the next line is not inside the previous statement + expect(isBlock(file.ast.findChildAtPosition(util.createPosition(2, 0)))).to.be.true; + //after a surrogate pair on the same line + expect(file.ast.findChildAtPosition(util.createPosition(2, 15)).tokens.name.text).to.eql('alpha'); + expect(file.ast.findChildAtPosition(util.createPosition(2, 20)).tokens.name.text).to.eql('alpha'); + //past the end of the last line + expect(file.ast.findChildAtPosition(util.createPosition(9, 0))).to.be.undefined; + } + + it('handles position boundaries with LF line endings', () => { + testPositionBoundaries('\n'); + }); + + it('handles position boundaries with CRLF line endings', () => { + testPositionBoundaries('\r\n'); + }); }); describe('findChild', () => { diff --git a/src/util.spec.ts b/src/util.spec.ts index 2933c0756..b5b7f1923 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -17,7 +17,10 @@ import { TokenKind } from './lexer/TokenKind'; import { createToken } from './astUtils/creators'; import { createDottedIdentifier, createVariableExpression } from './astUtils/creators'; import { Parser } from './parser/Parser'; -import type { FunctionStatement } from './parser/Statement'; +import type { ExitStatement, FunctionStatement } from './parser/Statement'; +import { Lexer } from './lexer/Lexer'; +import type { Locatable } from './lexer/Token'; +import { isExitStatement } from './astUtils/reflection'; import { ComponentType } from './types/ComponentType'; const sinon = createSandbox(); @@ -1028,6 +1031,96 @@ describe('util', () => { }); }); + describe('getLocation', () => { + function getRanges(code: string) { + return Lexer.scan(code).tokens.map(x => [x.kind, util.getLocation(x)?.range]); + } + + it('returns undefined for synthetic items', () => { + expect(util.getLocation(createToken(TokenKind.Identifier, 'a'))).to.be.undefined; + expect(util.getLocation(undefined)).to.be.undefined; + }); + + it('uses the uri from the source', () => { + const token = Lexer.scan('a', { srcPath: s`${rootDir}/source/main.brs` }).tokens[0]; + expect(util.getLocation(token).uri).to.eql(util.pathToUri(s`${rootDir}/source/main.brs`)); + }); + + it('keeps a LF newline token on its own line', () => { + expect(getRanges('a\nb')).to.eql([ + [TokenKind.Identifier, util.createRange(0, 0, 0, 1)], + [TokenKind.Newline, util.createRange(0, 1, 0, 2)], + [TokenKind.Identifier, util.createRange(1, 0, 1, 1)], + [TokenKind.Eof, util.createRange(1, 1, 1, 2)] + ]); + }); + + it('keeps a CRLF newline token on its own line', () => { + expect(getRanges('a\r\nb')).to.eql([ + [TokenKind.Identifier, util.createRange(0, 0, 0, 1)], + [TokenKind.Newline, util.createRange(0, 1, 0, 3)], + [TokenKind.Identifier, util.createRange(1, 0, 1, 1)], + [TokenKind.Eof, util.createRange(1, 1, 1, 2)] + ]); + }); + + it('handles a trailing newline at the end of the file', () => { + expect(getRanges('a\n')).to.eql([ + [TokenKind.Identifier, util.createRange(0, 0, 0, 1)], + [TokenKind.Newline, util.createRange(0, 1, 0, 2)], + [TokenKind.Eof, util.createRange(1, 0, 1, 1)] + ]); + }); + + it('counts surrogate pairs as two characters', () => { + const tokens = Lexer.scan('x = "😀" + y').tokens; + expect(util.getLocation(tokens.find(x => x.text === 'y')).range).to.eql(util.createRange(0, 11, 0, 12)); + }); + + it('tracks lines through multi-line template strings', () => { + // eslint-disable-next-line no-template-curly-in-string + const tokens = Lexer.scan('x = `one\ntwo ${y}\r\nthree`\nz').tokens; + expect(util.getLocation(tokens.find(x => x.text === 'y')).range).to.eql(util.createRange(1, 6, 1, 7)); + expect(util.getLocation(tokens.find(x => x.text === 'z')).range).to.eql(util.createRange(3, 0, 3, 1)); + }); + + it('handles the split `elseif` tokens', () => { + const tokens = Lexer.scan('if a then\nelseif b then\nend if').tokens; + expect(util.getLocation(tokens.find(x => x.kind === TokenKind.Else)).range).to.eql(util.createRange(1, 0, 1, 4)); + expect(util.getLocation(tokens.filter(x => x.kind === TokenKind.If)[1]).range).to.eql(util.createRange(1, 4, 1, 6)); + }); + + it('handles the split `exitwhile` tokens', () => { + const { ast } = Parser.parse('sub main()\n while true\n exitwhile\n end while\nend sub'); + const exitStatement = ast.findChild(isExitStatement); + expect(util.getLocation(exitStatement.tokens.exit).range).to.eql(util.createRange(2, 8, 2, 12)); + expect(util.getLocation(exitStatement.tokens.loopType).range).to.eql(util.createRange(2, 12, 2, 17)); + }); + + it('builds locations for multi-line nodes', () => { + const { ast } = Parser.parse('sub main()\r\n print 1\r\nend sub'); + expect(util.getLocation(ast.statements[0]).range).to.eql(util.createRange(0, 0, 2, 7)); + }); + }); + + describe('setBounds', () => { + it('uses the min start and max end of the locatables', () => { + const result = util.setBounds({} as Locatable, testLocatable(1, 5, 1, 8), undefined, testLocatable(0, 2, 0, 4), createToken(TokenKind.Identifier, 'synthetic')); + expect(util.getLocation(result).range).to.eql(util.createRange(0, 2, 1, 8)); + }); + + it('ignores locatables from a different source', () => { + const other = Lexer.scan('\n\nabcdef').tokens[2]; + const result = util.setBounds({} as Locatable, testLocatable(0, 2, 0, 4), other); + expect(util.getLocation(result).range).to.eql(util.createRange(0, 2, 0, 4)); + }); + + it('clears the bounds when nothing has a source', () => { + const result = util.setBounds({ pos: 1, end: 2, source: testLocatable(0, 0, 0, 0).source }, createToken(TokenKind.Identifier, 'a')); + expect(result).to.eql({ pos: undefined, end: undefined, source: undefined }); + }); + }); + describe('range creation', () => { it('createRangeFromPositions', () => { const pos11 = { line: 1, character: 1 }; From e5c4b5232a3934dc255c7de9e45ccb38e00a776e Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sun, 27 Sep 2026 09:46:58 -0400 Subject: [PATCH 07/26] Fix end position for ranges that end at the start of a line An end offset at a line start was always reported on the previous line, which is only right for tokens that end with a newline. Derived ranges like an empty block (end of `)` to start of `end sub`) now end at `(nextLine, 0)` as they did before. Co-Authored-By: Claude Opus 5.5 --- src/util.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index c82fc8473..43a1f584c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1388,8 +1388,11 @@ export class Util { } const lineStarts = source.lineStarts; const startLine = this.getLineIndex(lineStarts, locatable.pos, 0); - //`end` is exclusive, so use the line of the last character. This keeps a token that ends with a newline on its own line - const endLine = locatable.end > locatable.pos ? this.getLineIndex(lineStarts, locatable.end - 1, startLine) : startLine; + let endLine = this.getLineIndex(lineStarts, locatable.end, startLine); + //a token that ends with a newline ends on its own line rather than at the start of the next one + if (endLine > startLine && lineStarts[endLine] === locatable.end && /[\r\n]$/.test((locatable as Token).text)) { + endLine--; + } return { uri: source.uri, range: this.createRange(startLine, locatable.pos - lineStarts[startLine], endLine, locatable.end - lineStarts[endLine]) From 6efeff4e7c938babfef802a754618d43190eafcc Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Sun, 27 Sep 2026 09:46:58 -0400 Subject: [PATCH 08/26] Add language server location boundary tests Tests for hover, definition, references, document/workspace symbols, semantic tokens, selection ranges, completions, signature help, inlay hints, code actions, diagnostics and sourcemaps, covering CRLF, surrogate pairs, multi-line template strings, column 0, end of identifiers, last line without a trailing newline, and empty blocks. They only use the public Position/Location APIs, and all of them also pass against v1. Co-Authored-By: Claude Opus 5.5 --- src/Program.spec.ts | 260 ++++++++++++ .../completions/CompletionsProcessor.spec.ts | 288 +++++++++++++ .../definition/DefinitionProvider.spec.ts | 213 ++++++++++ src/bscPlugin/hover/HoverProcessor.spec.ts | 284 +++++++++++++ .../references/ReferencesProvider.spec.ts | 208 ++++++++++ .../SelectionRangesProcessor.spec.ts | 377 ++++++++++++++++++ .../BrsFileSemanticTokensProcessor.spec.ts | 172 ++++++++ .../symbols/DocumentSymbolProcessor.spec.ts | 152 +++++++ .../symbols/WorkspaceSymbolProcessor.spec.ts | 116 ++++++ .../validation/ScopeValidator.spec.ts | 323 +++++++++++++++ src/files/BrsFile.spec.ts | 306 ++++++++++++++ src/lexer/Lexer.spec.ts | 68 +++- src/parser/Parser.spec.ts | 62 +++ 13 files changed, 2828 insertions(+), 1 deletion(-) diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 070610a8c..fadea8b5d 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -3463,6 +3463,266 @@ describe('Program', () => { // We don't necessarily expect specific results, just that it doesn't crash expect(signatureHelp).to.be.an('array'); }); + + describe('location boundaries', () => { + //every test in this block builds its source from an explicit list of lines (no indentation stripping) + //so that the line/character values below can be counted by hand + const greetLabel = 'sub greet(name as string, age as integer)'; + + function expectSignature(srcPath: string, line: number, character: number, label: string, index: number) { + const signatureHelp = program.getSignatureHelp(srcPath, util.createPosition(line, character)); + expect( + signatureHelp.map(x => ({ label: x.signature?.label, index: x.index })), + `wrong signature help at ${line},${character}` + ).to.eql([{ label: label, index: index }]); + } + + function expectNoSignature(srcPath: string, line: number, character: number) { + const signatureHelp = program.getSignatureHelp(srcPath, util.createPosition(line, character)); + expect(signatureHelp, `expected no signature help at ${line},${character}`).to.eql([]); + } + + function testSingleLineCall(srcPath: string, eol: string) { + program.setFile(srcPath, [ + 'sub main()', //0 + ' greet("bob", 42)', //1 + 'end sub', //2 + 'sub greet(name as string, age as integer)', //3 + 'end sub' //4 + ].join(eol)); + program.validate(); + expectZeroDiagnostics(program); + // greet|("bob", 42) + expectNoSignature(srcPath, 1, 9); + // greet(|"bob", 42) + expectSignature(srcPath, 1, 10, greetLabel, 0); + // greet("bob"|, 42) + expectSignature(srcPath, 1, 15, greetLabel, 0); + // greet("bob",| 42) + expectSignature(srcPath, 1, 16, greetLabel, 1); + // greet("bob", |42) + expectSignature(srcPath, 1, 17, greetLabel, 1); + // greet("bob", 42|) + expectSignature(srcPath, 1, 19, greetLabel, 1); + // greet("bob", 42)| + expectSignature(srcPath, 1, 20, greetLabel, 2); + //the lines before and after the call + expectNoSignature(srcPath, 0, 10); + expectNoSignature(srcPath, 2, 0); + } + + it('handles every boundary of a single-line call in a .bs file', () => { + testSingleLineCall('source/main.bs', '\n'); + }); + + it('handles every boundary of a single-line call in a .brs file', () => { + testSingleLineCall('source/main.brs', '\n'); + }); + + it('handles every boundary of a single-line call in a file with CRLF line endings', () => { + testSingleLineCall('source/main.bs', '\r\n'); + }); + + it('handles every boundary of a single-line call in a .brs file with CRLF line endings', () => { + testSingleLineCall('source/main.brs', '\r\n'); + }); + + it('handles nested calls', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + ' outer(inner(1, 2), 3)', //1 + 'end sub', //2 + 'sub outer(a as dynamic, b as integer)', //3 + 'end sub', //4 + 'function inner(x as integer, y as integer)', //5 + ' return x + y', //6 + 'end function' //7 + ].join('\n')); + program.validate(); + const outerLabel = 'sub outer(a as dynamic, b as integer)'; + const innerLabel = 'function inner(x as integer, y as integer)'; + // outer(|inner(1, 2), 3) + expectSignature('source/main.bs', 1, 10, outerLabel, 0); + // outer(inner(|1, 2), 3) + expectSignature('source/main.bs', 1, 16, innerLabel, 0); + // outer(inner(1,| 2), 3) (the space after a nested comma isn't inside any argument, so this resolves to the outer call, same as v1) + expectSignature('source/main.bs', 1, 18, outerLabel, 0); + // outer(inner(1, |2), 3) + expectSignature('source/main.bs', 1, 19, innerLabel, 1); + // outer(inner(1, 2|), 3) + expectSignature('source/main.bs', 1, 20, innerLabel, 1); + // outer(inner(1, 2)|, 3) + expectSignature('source/main.bs', 1, 21, outerLabel, 0); + // outer(inner(1, 2),| 3) + expectSignature('source/main.bs', 1, 22, outerLabel, 1); + // outer(inner(1, 2), 3|) + expectSignature('source/main.bs', 1, 24, outerLabel, 1); + }); + + it('handles a call that spans multiple lines', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + ' greet(', //1 + ' "bob",', //2 + ' 42', //3 + ' )', //4 + 'end sub', //5 + 'sub greet(name as string, age as integer)', //6 + 'end sub' //7 + ].join('\n')); + program.validate(); + expectZeroDiagnostics(program); + // greet|( + expectNoSignature('source/main.bs', 1, 9); + // greet(| + expectSignature('source/main.bs', 1, 10, greetLabel, 0); + //| "bob", + expectSignature('source/main.bs', 2, 0, greetLabel, 0); + // |"bob", + expectSignature('source/main.bs', 2, 8, greetLabel, 0); + // "bob",| + expectSignature('source/main.bs', 2, 14, greetLabel, 1); + //| 42 + expectSignature('source/main.bs', 3, 0, greetLabel, 1); + // 42| + expectSignature('source/main.bs', 3, 10, greetLabel, 1); + //the cursor is past the end of the last argument, so the index is one past it (same as for a single-line call) + // |) + expectSignature('source/main.bs', 4, 4, greetLabel, 2); + expectNoSignature('source/main.bs', 5, 0); + }); + + it('handles a call that spans multiple lines in a file with CRLF line endings', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + ' greet(', //1 + ' "bob",', //2 + ' 42', //3 + ' )', //4 + 'end sub', //5 + 'sub greet(name as string, age as integer)', //6 + 'end sub' //7 + ].join('\r\n')); + program.validate(); + expectZeroDiagnostics(program); + // greet(| + expectSignature('source/main.bs', 1, 10, greetLabel, 0); + //| "bob", + expectSignature('source/main.bs', 2, 0, greetLabel, 0); + // "bob",| + expectSignature('source/main.bs', 2, 14, greetLabel, 1); + //| 42 + expectSignature('source/main.bs', 3, 0, greetLabel, 1); + // 42| + expectSignature('source/main.bs', 3, 10, greetLabel, 1); + expectNoSignature('source/main.bs', 5, 0); + }); + + it('handles a surrogate-pair emoji before the target on the same line', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + //each emoji is 2 UTF-16 code units + ' greet("😀😀", 42)', //1 + ' x = "😀" : greet("bob", 42)', //2 + 'end sub', //3 + 'sub greet(name as string, age as integer)', //4 + 'end sub' //5 + ].join('\n')); + program.validate(); + // greet(|"😀😀", 42) + expectSignature('source/main.bs', 1, 10, greetLabel, 0); + // greet("😀😀"|, 42) + expectSignature('source/main.bs', 1, 16, greetLabel, 0); + // greet("😀😀",| 42) + expectSignature('source/main.bs', 1, 17, greetLabel, 1); + // greet("😀😀", 42|) + expectSignature('source/main.bs', 1, 20, greetLabel, 1); + + // x = "😀" : greet|("bob", 42) + expectNoSignature('source/main.bs', 2, 20); + // x = "😀" : greet(|"bob", 42) + expectSignature('source/main.bs', 2, 21, greetLabel, 0); + // x = "😀" : greet("bob",| 42) + expectSignature('source/main.bs', 2, 27, greetLabel, 1); + // x = "😀" : greet("bob", 42|) + expectSignature('source/main.bs', 2, 30, greetLabel, 1); + }); + + it('handles calls inside and after a multi-line template string', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + // eslint-disable-next-line no-template-curly-in-string + ' msg = `first ${greet("a", 1)}', //1 + // eslint-disable-next-line no-template-curly-in-string + 'second ${greet("b", 2)} third`', //2 + ' greet("c", 3)', //3 + 'end sub', //4 + 'function greet(name as string, age as integer)', //5 + ' return name', //6 + 'end function' //7 + ].join('\n')); + program.validate(); + const label = 'function greet(name as string, age as integer)'; + // msg = `first ${greet("a",| 1)} + expectSignature('source/main.bs', 1, 29, label, 1); + //second ${greet(|"b", 2)} third` + expectSignature('source/main.bs', 2, 15, label, 0); + //second ${greet("b",| 2)} third` + expectSignature('source/main.bs', 2, 19, label, 1); + //second ${greet("b", 2|)} third` + expectSignature('source/main.bs', 2, 21, label, 1); + //second ${greet("b", 2)} th|ird` + expectNoSignature('source/main.bs', 2, 26); + // greet("c",| 3) + expectSignature('source/main.bs', 3, 14, label, 1); + }); + + it('handles a call on the last line of a file with no trailing newline', () => { + program.setFile('source/main.bs', [ + //signature help crashes when the declaration is the very first token of the file (it has no leading trivia), so start with an empty line + '', //0 + 'sub greet(name as string, age as integer)', //1 + 'end sub', //2 + 'sub main() : greet("bob", 42) : end sub' //3 + ].join('\n')); + program.validate(); + expectZeroDiagnostics(program); + //sub main() : greet(|"bob", 42) : end sub + expectSignature('source/main.bs', 3, 19, greetLabel, 0); + //sub main() : greet("bob",| 42) : end sub + expectSignature('source/main.bs', 3, 25, greetLabel, 1); + //sub main() : greet("bob", 42|) : end sub + expectSignature('source/main.bs', 3, 28, greetLabel, 1); + //sub main() : greet("bob", 42) : end sub| + expectNoSignature('source/main.bs', 3, 39); + }); + + it('computes the exact signature label when the declaration contains an emoji and uses CRLF line endings', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + ' greet("bob", 42)', //1 + 'end sub', //2 + 'sub greet(name = "😀" as string, age = 1 as integer)', //3 + ' print name', //4 + 'end sub' //5 + ].join('\r\n')); + program.validate(); + // greet("bob",| 42) + expectSignature('source/main.bs', 1, 16, 'sub greet(name = "😀" as string, age = 1 as integer)', 1); + }); + + it('computes the exact signature label for a declaration with an empty body on the last line of a file', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + ' greet("bob", 42)', //1 + 'end sub', //2 + 'sub greet(name as string, age as integer) : end sub' //3 + ].join('\n')); + program.validate(); + // greet("bob",| 42) + expectSignature('source/main.bs', 1, 16, greetLabel, 1); + }); + }); }); describe('plugins', () => { diff --git a/src/bscPlugin/completions/CompletionsProcessor.spec.ts b/src/bscPlugin/completions/CompletionsProcessor.spec.ts index 1904b2b00..aadf5c9dd 100644 --- a/src/bscPlugin/completions/CompletionsProcessor.spec.ts +++ b/src/bscPlugin/completions/CompletionsProcessor.spec.ts @@ -2724,4 +2724,292 @@ describe('CompletionsProcessor', () => { }]); }); }); + + describe('location boundaries', () => { + //every test in this block builds its source from an explicit list of lines (no indentation stripping) + //so that the line/character values below can be counted by hand + const personClassLines = [ + 'class Person', //0 + ' name as string', //1 + ' sub speak()', //2 + ' end sub', //3 + 'end class' //4 + ]; + + function getLabels(srcPath: string, line: number, character: number) { + return program.getCompletions(srcPath, util.createPosition(line, character)).map(x => x.label); + } + + it('finds members directly after a dot', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main()', //5 + ' p = new Person()', //6 + ' p.', //7 + 'end sub' //8 + ].join('\n')); + program.validate(); + // p.| + const labels = getLabels('source/main.bs', 7, 6); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('finds members when the cursor is in the middle of an identifier after a dot', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main()', //5 + ' p = new Person()', //6 + ' p.spe', //7 + 'end sub' //8 + ].join('\n')); + program.validate(); + // p.s|pe + const labels = getLabels('source/main.bs', 7, 7); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('finds locals when the cursor is in the middle of an identifier', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ' person = 1', //1 + ' pers', //2 + 'end sub' //3 + ].join('\n')); + program.validate(); + // pe|rs + expect(getLabels('source/main.brs', 2, 6)).to.include.members(['person', 'main']); + }); + + it('finds locals for an identifier at column 0 of a line', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ' person = 1', //1 + 'pe', //2 + 'end sub' //3 + ].join('\n')); + program.validate(); + //|pe + expect(getLabels('source/main.brs', 2, 0)).to.include.members(['person', 'main']); + //pe| + expect(getLabels('source/main.brs', 2, 2)).to.include.members(['person', 'main']); + }); + + it('finds locals at column 0 of an empty line', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ' person = 1', //1 + '', //2 + ' print person', //3 + 'end sub' //4 + ].join('\n')); + program.validate(); + expect(getLabels('source/main.brs', 2, 0)).to.include.members(['person', 'main']); + }); + + it('finds parameters inside an empty function body', () => { + program.setFile('source/main.bs', [ + 'sub main(person as string)', //0 + '', //1 + 'end sub' //2 + ].join('\n')); + program.validate(); + expect(getLabels('source/main.bs', 1, 0)).to.include.members(['person', 'main']); + }); + + it('finds parameters inside an empty, whitespace-only function body', () => { + program.setFile('source/main.bs', [ + 'sub main(person as string)', //0 + ' ', //1 + 'end sub' //2 + ].join('\n')); + program.validate(); + // | + expect(getLabels('source/main.bs', 1, 4)).to.include.members(['person', 'main']); + // | + expect(getLabels('source/main.bs', 1, 2)).to.include.members(['person', 'main']); + }); + + it('finds parameters inside a function body with no lines between the signature and `end sub`', () => { + program.setFile('source/main.bs', [ + 'sub main(person as string)', //0 + 'end sub' //1 + ].join('\n')); + program.validate(); + //sub main(person as string)| + expect(getLabels('source/main.bs', 0, 26)).to.include.members(['person', 'main']); + }); + + it('finds members after a dot when a surrogate-pair emoji precedes the target on the same line', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main()', //5 + ' p = new Person()', //6 + //each emoji is 2 UTF-16 code units, so the dot is at character 17 + ' x = "😀😀": p.', //7 + 'end sub' //8 + ].join('\n')); + program.validate(); + // x = "😀😀": p.| + const labels = getLabels('source/main.bs', 7, 18); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('finds members after a dot in a file with CRLF line endings', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main()', //5 + ' p = new Person()', //6 + ' p.', //7 + 'end sub' //8 + ].join('\r\n')); + program.validate(); + // p.| + const labels = getLabels('source/main.bs', 7, 6); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('finds locals mid-identifier in a file with CRLF line endings', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ' person = 1', //1 + ' pers', //2 + 'end sub' //3 + ].join('\r\n')); + program.validate(); + // pe|rs + expect(getLabels('source/main.brs', 2, 6)).to.include.members(['person', 'main']); + }); + + it('finds completions inside `${}` on a later line of a multi-line template string, and after the template string', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main()', //5 + ' name = "bob"', //6 + ' p = new Person()', //7 + // eslint-disable-next-line no-template-curly-in-string + ' msg = `hello ${name}', //8 + // eslint-disable-next-line no-template-curly-in-string + 'and ${na} again`', //9 + ' p.', //10 + 'end sub' //11 + ].join('\n')); + program.validate(); + //and ${n|a} again` + expect(getLabels('source/main.bs', 9, 7)).to.include.members(['name', 'p', 'main']); + // p.| + const labels = getLabels('source/main.bs', 10, 6); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('finds members on the last line of a file with no trailing newline', () => { + program.setFile('source/main.bs', [ + ...personClassLines, + 'sub main(p as Person) : p.speak() : end sub' //5 + ].join('\n')); + program.validate(); + //sub main(p as Person) : p.|speak() : end sub + const labels = getLabels('source/main.bs', 5, 26); + expect(labels).to.include.members(['name', 'speak']); + expect(labels).not.to.include('main'); + }); + + it('returns exact pkg path textEdit ranges for a string literal', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ' print "pkg:"', //1 + 'end sub' //2 + ].join('\n')); + program.validate(); + // print "pk|g:" + const completions = program.getCompletions('source/main.brs', util.createPosition(1, 13)); + expect(completions.map(x => x.label)).to.eql(['pkg:/source/main.brs']); + //replaces the text between the quotes + expect(completions[0].textEdit).to.eql({ + newText: 'pkg:/source/main.brs', + range: util.createRange(1, 11, 1, 15) + }); + }); + + it('returns exact pkg path textEdit ranges when a surrogate-pair emoji precedes the string on the same line', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + //each emoji is 2 UTF-16 code units + ' x = "😀" + "pkg:"', //1 + 'end sub' //2 + ].join('\n')); + program.validate(); + // x = "😀" + "pk|g:" + const completions = program.getCompletions('source/main.brs', util.createPosition(1, 18)); + expect(completions.map(x => x.label)).to.eql(['pkg:/source/main.brs']); + expect(completions[0].textEdit).to.eql({ + newText: 'pkg:/source/main.brs', + range: util.createRange(1, 16, 1, 20) + }); + }); + + it('returns exact pkg path textEdit ranges in a file with CRLF line endings', () => { + program.setFile('source/main.brs', [ + 'sub main()', //0 + ` ' 😀 comment`, //1 + ' print "pkg:"', //2 + 'end sub' //3 + ].join('\r\n')); + program.validate(); + // print "pk|g:" + const completions = program.getCompletions('source/main.brs', util.createPosition(2, 13)); + expect(completions.map(x => x.label)).to.eql(['pkg:/source/main.brs']); + expect(completions[0].textEdit).to.eql({ + newText: 'pkg:/source/main.brs', + range: util.createRange(2, 11, 2, 15) + }); + }); + + it('returns exact pkg path textEdit ranges on a later line of a multi-line template string', () => { + program.setFile('source/main.bs', [ + 'sub main()', //0 + // eslint-disable-next-line no-template-curly-in-string + ' x = `line one ${1}', //1 + 'pkg:`', //2 + 'end sub' //3 + ].join('\n')); + program.validate(); + //pk|g:` + const completions = program.getCompletions('source/main.bs', util.createPosition(2, 2)); + //the pkg path is the transpiled destination, so `.bs` becomes `.brs` + expect(completions.map(x => x.label)).to.eql(['pkg:/source/main.brs']); + expect(completions[0].textEdit).to.eql({ + newText: 'pkg:/source/main.brs', + range: util.createRange(2, 0, 2, 4) + }); + }); + + it('returns exact import path textEdit ranges in a file with CRLF line endings', () => { + program.setFile('source/lib.bs', [ + 'sub libFunc()', + 'end sub' + ].join('\n')); + program.setFile('source/main.bs', [ + `' 😀 header`, //0 + 'import "lib.bs"', //1 + 'sub main()', //2 + 'end sub' //3 + ].join('\r\n')); + program.validate(); + //import "li|b.bs" + const completions = program.getCompletions('source/main.bs', util.createPosition(1, 10)); + expect(completions.map(x => x.label).sort()).to.eql(['lib.bs', 'pkg:/source/lib.bs']); + //the range covers the path without its quotes (the import statement trims them from the token) + for (const completion of completions) { + expect(completion.textEdit).to.eql({ + newText: completion.label, + range: util.createRange(1, 8, 1, 14) + }); + } + }); + }); }); diff --git a/src/bscPlugin/definition/DefinitionProvider.spec.ts b/src/bscPlugin/definition/DefinitionProvider.spec.ts index b0e7de516..2aa0ec7b0 100644 --- a/src/bscPlugin/definition/DefinitionProvider.spec.ts +++ b/src/bscPlugin/definition/DefinitionProvider.spec.ts @@ -751,4 +751,217 @@ describe('DefinitionProvider', () => { ); }); }); + + describe('location boundaries', () => { + //NOTE: these tests intentionally only use public `Program` APIs (Position in, Location out) so they + //can be run against any implementation of token/node locations. All expected values are hand-counted. + + /** + * Join lines with the given line ending. No trailing newline is added + */ + function lines(eol: string, ...items: string[]) { + return items.join(eol); + } + + function definitionAt(srcPath: string, line: number, character: number) { + return program.getDefinition(srcPath, util.createPosition(line, character)); + } + + function loc(srcPath: string, startLine: number, startCharacter: number, endLine: number, endCharacter: number) { + return { + uri: util.pathToUri(srcPath), + range: util.createRange(startLine, startCharacter, endLine, endCharacter) + }; + } + + it('finds the identifier at its first character, middle, and immediately after its last character', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' alpha = 1', + ' print alpha', + 'end sub' + )); + program.validate(); + const expected = [loc(file.srcPath, 1, 4, 1, 9)]; + //print |alpha + expect(definitionAt(file.srcPath, 2, 10)).to.eql(expected); + //print al|pha + expect(definitionAt(file.srcPath, 2, 12)).to.eql(expected); + //print alpha| (the end of a range is inclusive) + expect(definitionAt(file.srcPath, 2, 15)).to.eql(expected); + + //print| alpha (immediately after `print`, which has no definition) + expect(definitionAt(file.srcPath, 2, 9)).to.eql([]); + //leading whitespace + expect(definitionAt(file.srcPath, 2, 2)).to.eql([]); + }); + + it('finds identifiers at column 0 of a line and at the end of a line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + 'beta = 1', + 'print beta', + 'end sub' + )); + program.validate(); + const expected = [loc(file.srcPath, 1, 0, 1, 4)]; + //|beta = 1 + expect(definitionAt(file.srcPath, 1, 0)).to.eql(expected); + //print beta| + expect(definitionAt(file.srcPath, 2, 10)).to.eql(expected); + //print |beta + expect(definitionAt(file.srcPath, 2, 6)).to.eql(expected); + }); + + it('handles CRLF line endings and returns the uri of the other file for cross-file definitions', () => { + const libFile = program.setFile('source/lib.brs', lines('\r\n', + `' helpers`, + 'function helper()', + ' return 1', + 'end function' + )); + const mainFile = program.setFile('source/main.brs', lines('\r\n', + 'sub main()', + ' helper()', + 'helper()', + ' x = helper()', + 'end sub' + )); + program.validate(); + const expected = [loc(libFile.srcPath, 1, 0, 3, 12)]; + //|helper() + expect(definitionAt(mainFile.srcPath, 1, 4)).to.eql(expected); + //hel|per() + expect(definitionAt(mainFile.srcPath, 1, 7)).to.eql(expected); + //helper|() + expect(definitionAt(mainFile.srcPath, 1, 10)).to.eql(expected); + //column 0 after a `\r\n` + expect(definitionAt(mainFile.srcPath, 2, 0)).to.eql(expected); + //x = helper|() + expect(definitionAt(mainFile.srcPath, 3, 14)).to.eql(expected); + }); + + it('uses utf-16 code units for characters after a surrogate pair emoji on the same line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' name = "x"', + ' print "😀😀" + name', + ' x = "😀": y = 1', + ' print y', + 'end sub' + )); + program.validate(); + //each emoji is 2 utf-16 code units, so `name` starts at 19 (not 17) + const expected = [loc(file.srcPath, 1, 4, 1, 8)]; + expect(definitionAt(file.srcPath, 2, 19)).to.eql(expected); + expect(definitionAt(file.srcPath, 2, 21)).to.eql(expected); + expect(definitionAt(file.srcPath, 2, 23)).to.eql(expected); + + //the declaration of `y` comes after an emoji on its line + expect(definitionAt(file.srcPath, 4, 10)).to.eql([loc(file.srcPath, 3, 14, 3, 15)]); + //y| = 1 + expect(definitionAt(file.srcPath, 3, 15)).to.eql([loc(file.srcPath, 3, 14, 3, 15)]); + }); + + it('counts lines through multi-line template strings', () => { + const file = program.setFile('source/main.bs', lines('\n', + 'sub main()', + ' name = "bob"', + /* eslint-disable no-template-curly-in-string */ + ' msg = `hello ${name}', + 'line two ${name}', + 'and ${name} three`', + /* eslint-enable no-template-curly-in-string */ + ' after = msg', + ' print after + name', + 'end sub' + )); + program.validate(); + const nameDeclaration = [loc(file.srcPath, 1, 4, 1, 8)]; + //inside `${}` on later lines of the template string + expect(definitionAt(file.srcPath, 3, 13)).to.eql(nameDeclaration); + expect(definitionAt(file.srcPath, 3, 15)).to.eql(nameDeclaration); + expect(definitionAt(file.srcPath, 4, 8)).to.eql(nameDeclaration); + expect(definitionAt(file.srcPath, 4, 10)).to.eql(nameDeclaration); + + //after the template string (both the declaration and the usage come after it) + expect(definitionAt(file.srcPath, 6, 10)).to.eql([loc(file.srcPath, 5, 4, 5, 9)]); + expect(definitionAt(file.srcPath, 6, 15)).to.eql([loc(file.srcPath, 5, 4, 5, 9)]); + //after = msg| + expect(definitionAt(file.srcPath, 5, 15)).to.eql([loc(file.srcPath, 2, 4, 2, 7)]); + //print after + name| + expect(definitionAt(file.srcPath, 6, 22)).to.eql(nameDeclaration); + }); + + it('finds items on the last line of a file with no trailing newline', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' last()', + 'end sub', + 'sub last(): value = 1: print value: end sub' + )); + program.validate(); + //print value|: + expect(definitionAt(file.srcPath, 3, 34)).to.eql([loc(file.srcPath, 3, 12, 3, 17)]); + //print |value + expect(definitionAt(file.srcPath, 3, 29)).to.eql([loc(file.srcPath, 3, 12, 3, 17)]); + //the function statement ends at the very end of the file + expect(definitionAt(file.srcPath, 1, 4)).to.eql([loc(file.srcPath, 3, 0, 3, 43)]); + }); + + it('finds symbols in namespaces, classes, and empty function bodies across files', () => { + const libFile = program.setFile('source/lib.bs', lines('\n', + 'namespace alpha.beta', + ' function noop()', + ' end function', + ' class Widget', + ' sub render()', + ' end sub', + ' end class', + 'end namespace', + 'sub empty()', + 'end sub' + )); + const mainFile = program.setFile('source/main.bs', lines('\n', + 'sub main()', + ' alpha.beta.noop()', + ' w = new alpha.beta.Widget()', + ' empty()', + 'end sub' + )); + program.validate(); + //new alpha.beta.|Widget() + expect(definitionAt(mainFile.srcPath, 2, 24)).to.eql([loc(libFile.srcPath, 3, 10, 3, 16)]); + //new alpha.beta.Widget|() + expect(definitionAt(mainFile.srcPath, 2, 29)).to.eql([loc(libFile.srcPath, 3, 10, 3, 16)]); + //alpha.beta.noop|() (an empty function inside a namespace) + expect(definitionAt(mainFile.srcPath, 1, 19)).to.eql([loc(libFile.srcPath, 1, 4, 2, 16)]); + //empty|() (an empty function) + expect(definitionAt(mainFile.srcPath, 3, 9)).to.eql([loc(libFile.srcPath, 8, 0, 9, 7)]); + }); + + it('finds namespaced consts and enum members in another file', () => { + const libFile = program.setFile('source/lib.bs', lines('\n', + 'namespace alpha', + ' const PI = 3.14', + ' enum Direction', + ' up = "up"', + ' end enum', + 'end namespace' + )); + const mainFile = program.setFile('source/main.bs', lines('\n', + 'sub main()', + ' print alpha.PI', + ' print alpha.Direction.up', + 'end sub' + )); + program.validate(); + //print alpha.PI| + expect(definitionAt(mainFile.srcPath, 1, 18)).to.eql([loc(libFile.srcPath, 1, 10, 1, 12)]); + //print alpha.Direction.up| + expect(definitionAt(mainFile.srcPath, 2, 28)).to.eql([loc(libFile.srcPath, 3, 8, 3, 10)]); + //print alpha.Direc|tion.up + expect(definitionAt(mainFile.srcPath, 2, 21)).to.eql([loc(libFile.srcPath, 2, 9, 2, 18)]); + }); + }); }); diff --git a/src/bscPlugin/hover/HoverProcessor.spec.ts b/src/bscPlugin/hover/HoverProcessor.spec.ts index 40443f9d3..0997dd459 100644 --- a/src/bscPlugin/hover/HoverProcessor.spec.ts +++ b/src/bscPlugin/hover/HoverProcessor.spec.ts @@ -1169,4 +1169,288 @@ describe('HoverProcessor', () => { expect(hover?.contents).eql([`${fence('data as string')}${commentSep}A cool JSON string of data`]); }); }); + + describe('location boundaries', () => { + //NOTE: these tests intentionally only use public `Program` APIs (Position in, Range out) so they + //can be run against any implementation of token/node locations. All expected values are hand-counted. + + /** + * Join lines with the given line ending. No trailing newline is added + */ + function lines(eol: string, ...items: string[]) { + return items.join(eol); + } + + function hoverAt(srcPath: string, line: number, character: number) { + return program.getHover(srcPath, util.createPosition(line, character)); + } + + it('finds the identifier at its first character, middle, and immediately after its last character', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' alpha = 1', + ' print alpha', + 'end sub' + )); + program.validate(); + const expected = [{ + range: util.createRange(2, 10, 2, 15), + contents: [fence('alpha as integer')] + }]; + //print |alpha + expect(hoverAt(file.srcPath, 2, 10)).to.eql(expected); + //print al|pha + expect(hoverAt(file.srcPath, 2, 12)).to.eql(expected); + //print alpha| (the end of a range is inclusive) + expect(hoverAt(file.srcPath, 2, 15)).to.eql(expected); + + //alpha| = 1 (immediately after the declaration, which is followed by whitespace) + expect(hoverAt(file.srcPath, 1, 9)).to.eql([{ + range: util.createRange(1, 4, 1, 9), + contents: [fence('alpha as integer')] + }]); + + //print| alpha (immediately after `print`, which is not hoverable) + expect(hoverAt(file.srcPath, 2, 9)).to.eql([]); + //pri|nt + expect(hoverAt(file.srcPath, 2, 6)).to.eql([]); + //leading whitespace + expect(hoverAt(file.srcPath, 2, 2)).to.eql([]); + }); + + it('finds identifiers at column 0 of a line and at the end of a line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + 'beta = 1', + 'print beta', + 'end sub' + )); + program.validate(); + //|beta = 1 + expect(hoverAt(file.srcPath, 1, 0)).to.eql([{ + range: util.createRange(1, 0, 1, 4), + contents: [fence('beta as integer')] + }]); + //print beta| + expect(hoverAt(file.srcPath, 2, 10)).to.eql([{ + range: util.createRange(2, 6, 2, 10), + contents: [fence('beta as integer')] + }]); + //|end sub + expect(hoverAt(file.srcPath, 3, 0)).to.eql([{ + range: util.createRange(3, 0, 3, 7), + contents: [fence('sub main() as void')] + }]); + }); + + it('handles CRLF line endings', () => { + const file = program.setFile('source/main.brs', lines('\r\n', + 'sub main()', + ' gamma = 1', + ' print gamma', + ' print gamma + 1', + 'delta = gamma', + 'end sub' + )); + program.validate(); + const gammaLine3 = [{ + range: util.createRange(3, 10, 3, 15), + contents: [fence('gamma as integer')] + }]; + expect(hoverAt(file.srcPath, 3, 10)).to.eql(gammaLine3); + expect(hoverAt(file.srcPath, 3, 13)).to.eql(gammaLine3); + expect(hoverAt(file.srcPath, 3, 15)).to.eql(gammaLine3); + + //end of the line, right before the `\r\n` + expect(hoverAt(file.srcPath, 2, 15)).to.eql([{ + range: util.createRange(2, 10, 2, 15), + contents: [fence('gamma as integer')] + }]); + + //column 0 after a `\r\n` + expect(hoverAt(file.srcPath, 4, 0)).to.eql([{ + range: util.createRange(4, 0, 4, 5), + contents: [fence('delta as integer')] + }]); + //last identifier on the line before the `\r\n` + expect(hoverAt(file.srcPath, 4, 13)).to.eql([{ + range: util.createRange(4, 8, 4, 13), + contents: [fence('gamma as integer')] + }]); + expect(hoverAt(file.srcPath, 5, 0)).to.eql([{ + range: util.createRange(5, 0, 5, 7), + contents: [fence('sub main() as void')] + }]); + }); + + it('uses utf-16 code units for characters after a surrogate pair emoji on the same line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' name = "x"', + ' print "😀😀" + name', + ` ' 😀 comment`, + ' print name', + 'end sub' + )); + program.validate(); + //each emoji is 2 utf-16 code units, so `name` starts at 19 (not 17) + const expected = [{ + range: util.createRange(2, 19, 2, 23), + contents: [fence('name as string')] + }]; + expect(hoverAt(file.srcPath, 2, 19)).to.eql(expected); + expect(hoverAt(file.srcPath, 2, 21)).to.eql(expected); + expect(hoverAt(file.srcPath, 2, 23)).to.eql(expected); + + //the line after the emoji comment is unaffected + expect(hoverAt(file.srcPath, 4, 10)).to.eql([{ + range: util.createRange(4, 10, 4, 14), + contents: [fence('name as string')] + }]); + }); + + it('counts lines through multi-line template strings', () => { + const file = program.setFile('source/main.bs', lines('\n', + 'sub main()', + ' name = "bob"', + /* eslint-disable no-template-curly-in-string */ + ' msg = `hello ${name}', + 'line two ${name}', + 'and ${name} three`', + /* eslint-enable no-template-curly-in-string */ + ' print msg + name', + 'end sub' + )); + program.validate(); + //after the template string + const nameAfter = [{ + range: util.createRange(5, 16, 5, 20), + contents: [fence('name as string')] + }]; + expect(hoverAt(file.srcPath, 5, 16)).to.eql(nameAfter); + expect(hoverAt(file.srcPath, 5, 18)).to.eql(nameAfter); + expect(hoverAt(file.srcPath, 5, 20)).to.eql(nameAfter); + + expect(hoverAt(file.srcPath, 5, 10)).to.eql([{ + range: util.createRange(5, 10, 5, 13), + contents: [fence('msg as string')] + }]); + + //the first character of an identifier directly after `${` is also the (inclusive) end of the `${` + //token, which is found first and is not hoverable + expect(hoverAt(file.srcPath, 2, 19)).to.eql([]); + expect(hoverAt(file.srcPath, 3, 11)).to.eql([]); + expect(hoverAt(file.srcPath, 4, 6)).to.eql([]); + + //inside `${}` on the first line of the template string + expect(hoverAt(file.srcPath, 2, 20)).to.eql([{ + range: util.createRange(2, 19, 2, 23), + contents: [fence('name as string')] + }]); + //inside `${}` on the second line of the template string + expect(hoverAt(file.srcPath, 3, 12)).to.eql([{ + range: util.createRange(3, 11, 3, 15), + contents: [fence('name as string')] + }]); + expect(hoverAt(file.srcPath, 3, 15)).to.eql([{ + range: util.createRange(3, 11, 3, 15), + contents: [fence('name as string')] + }]); + //inside `${}` on the third line of the template string + expect(hoverAt(file.srcPath, 4, 8)).to.eql([{ + range: util.createRange(4, 6, 4, 10), + contents: [fence('name as string')] + }]); + expect(hoverAt(file.srcPath, 4, 10)).to.eql([{ + range: util.createRange(4, 6, 4, 10), + contents: [fence('name as string')] + }]); + }); + + it('finds items on the last line of a file with no trailing newline', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + 'end sub', + 'sub last(): value = 1: print value: end sub' + )); + program.validate(); + //sub |last() + expect(hoverAt(file.srcPath, 2, 4)).to.eql([{ + range: util.createRange(2, 4, 2, 8), + contents: [fence('sub last() as void')] + }]); + //value| = 1 + expect(hoverAt(file.srcPath, 2, 17)).to.eql([{ + range: util.createRange(2, 12, 2, 17), + contents: [fence('value as integer')] + }]); + //print value|: + expect(hoverAt(file.srcPath, 2, 34)).to.eql([{ + range: util.createRange(2, 29, 2, 34), + contents: [fence('value as integer')] + }]); + //end sub| + expect(hoverAt(file.srcPath, 2, 43)).to.eql([{ + range: util.createRange(2, 36, 2, 43), + contents: [fence('sub last() as void')] + }]); + }); + + it('finds symbols in namespaces, classes, and empty function bodies', () => { + const file = program.setFile('source/main.bs', lines('\n', + 'namespace alpha.beta', + ' function noop()', + ' end function', + ' class Widget', + ' sub render()', + ' end sub', + ' end class', + 'end namespace', + 'sub main()', + ' alpha.beta.noop()', + ' w = new alpha.beta.Widget()', + ' w.render()', + 'end sub' + )); + program.validate(); + //function |noop() (a function with an empty body) + const noopDeclaration = [{ + range: util.createRange(1, 13, 1, 17), + contents: [fence('function alpha.beta.noop() as dynamic')] + }]; + expect(hoverAt(file.srcPath, 1, 13)).to.eql(noopDeclaration); + expect(hoverAt(file.srcPath, 1, 15)).to.eql(noopDeclaration); + expect(hoverAt(file.srcPath, 1, 17)).to.eql(noopDeclaration); + + //alpha.beta.|noop() (the inclusive end of the `.` token wins, and `.` is not hoverable) + expect(hoverAt(file.srcPath, 9, 15)).to.eql([]); + //alpha.beta.n|oop() + expect(hoverAt(file.srcPath, 9, 16)).to.eql([{ + range: util.createRange(9, 15, 9, 19), + contents: [fence('function alpha.beta.noop() as dynamic')] + }]); + //alpha.beta.noop|() + expect(hoverAt(file.srcPath, 9, 19)).to.eql([{ + range: util.createRange(9, 15, 9, 19), + contents: [fence('function alpha.beta.noop() as dynamic')] + }]); + + //sub |render() (a method with an empty body) + expect(hoverAt(file.srcPath, 4, 12)).to.eql([{ + range: util.createRange(4, 12, 4, 18), + contents: [fence('sub alpha.beta.Widget.render() as void')] + }]); + + //w.render|() + expect(hoverAt(file.srcPath, 11, 12)).to.eql([{ + range: util.createRange(11, 6, 11, 12), + contents: [fence('sub alpha.beta.Widget.render() as void')] + }]); + + //new alpha.beta.Widget|() + expect(hoverAt(file.srcPath, 10, 29)[0]?.range).to.eql(util.createRange(10, 23, 10, 29)); + //class |Widget + expect(hoverAt(file.srcPath, 3, 10)[0]?.range).to.eql(util.createRange(3, 10, 3, 16)); + }); + }); }); diff --git a/src/bscPlugin/references/ReferencesProvider.spec.ts b/src/bscPlugin/references/ReferencesProvider.spec.ts index b6df2f2ea..cfb4080bf 100644 --- a/src/bscPlugin/references/ReferencesProvider.spec.ts +++ b/src/bscPlugin/references/ReferencesProvider.spec.ts @@ -285,6 +285,214 @@ describe('ReferencesProvider', () => { ).to.eql([]); }); + describe('location boundaries', () => { + //NOTE: these tests intentionally only use public `Program` APIs (Position in, Location out) so they + //can be run against any implementation of token/node locations. All expected values are hand-counted. + + /** + * Join lines with the given line ending. No trailing newline is added + */ + function lines(eol: string, ...items: string[]) { + return items.join(eol); + } + + /** + * Get the references at the position, sorted by uri, then by position + */ + function referencesAt(srcPath: string, line: number, character: number) { + return program.getReferences(srcPath, util.createPosition(line, character)).sort((a, b) => { + if (a.uri !== b.uri) { + return a.uri < b.uri ? -1 : 1; + } + return (a.range.start.line - b.range.start.line) || (a.range.start.character - b.range.start.character); + }); + } + + function loc(srcPath: string, startLine: number, startCharacter: number, endLine: number, endCharacter: number) { + return { + uri: util.pathToUri(srcPath), + range: util.createRange(startLine, startCharacter, endLine, endCharacter) + }; + } + + it('finds the identifier at its first character, middle, and immediately after its last character', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' alpha = 1', + ' print alpha', + ' alpha = alpha + 1', + 'end sub' + )); + program.validate(); + const expected = [ + loc(file.srcPath, 1, 4, 1, 9), + loc(file.srcPath, 2, 10, 2, 15), + loc(file.srcPath, 3, 4, 3, 9), + loc(file.srcPath, 3, 12, 3, 17) + ]; + //print |alpha + expect(referencesAt(file.srcPath, 2, 10)).to.eql(expected); + //print al|pha + expect(referencesAt(file.srcPath, 2, 12)).to.eql(expected); + //print alpha| (the end of a range is inclusive) + expect(referencesAt(file.srcPath, 2, 15)).to.eql(expected); + //alpha| = 1 + expect(referencesAt(file.srcPath, 1, 9)).to.eql(expected); + + //print| alpha (immediately after `print`, so we search for `print` instead) + expect(referencesAt(file.srcPath, 2, 9)).to.eql([]); + }); + + it('finds identifiers at column 0 of a line and at the end of a line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + 'beta = 1', + 'print beta', + 'end sub' + )); + program.validate(); + const expected = [ + loc(file.srcPath, 1, 0, 1, 4), + loc(file.srcPath, 2, 6, 2, 10) + ]; + //|beta = 1 + expect(referencesAt(file.srcPath, 1, 0)).to.eql(expected); + //print beta| + expect(referencesAt(file.srcPath, 2, 10)).to.eql(expected); + }); + + it('handles CRLF line endings and returns the uri of each file for cross-file references', () => { + const mainFile = program.setFile('source/main.brs', lines('\r\n', + 'sub main()', + ' gamma = 1', + ' print gamma', + 'gamma = 2', + 'end sub' + )); + const otherFile = program.setFile('source/other.brs', lines('\r\n', + 'sub other()', + ' print gamma', + 'end sub' + )); + program.validate(); + const expected = [ + loc(mainFile.srcPath, 1, 4, 1, 9), + loc(mainFile.srcPath, 2, 10, 2, 15), + loc(mainFile.srcPath, 3, 0, 3, 5), + loc(otherFile.srcPath, 1, 10, 1, 15) + ]; + //column 0 after a `\r\n` + expect(referencesAt(mainFile.srcPath, 3, 0)).to.eql(expected); + //end of the line, right before the `\r\n` + expect(referencesAt(mainFile.srcPath, 2, 15)).to.eql(expected); + //from the other file + expect(referencesAt(otherFile.srcPath, 1, 12)).to.eql(expected); + }); + + it('uses utf-16 code units for characters after a surrogate pair emoji on the same line', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + ' name = "😀"', + ' print "😀😀" + name', + ' x = "😀": name = name + "😀"', + 'end sub' + )); + program.validate(); + //each emoji is 2 utf-16 code units + const expected = [ + loc(file.srcPath, 1, 4, 1, 8), + loc(file.srcPath, 2, 19, 2, 23), + loc(file.srcPath, 3, 14, 3, 18), + loc(file.srcPath, 3, 21, 3, 25) + ]; + expect(referencesAt(file.srcPath, 2, 19)).to.eql(expected); + expect(referencesAt(file.srcPath, 2, 21)).to.eql(expected); + expect(referencesAt(file.srcPath, 2, 23)).to.eql(expected); + expect(referencesAt(file.srcPath, 3, 25)).to.eql(expected); + }); + + it('counts lines through multi-line template strings', () => { + const file = program.setFile('source/main.bs', lines('\n', + 'sub main()', + ' name = "bob"', + /* eslint-disable no-template-curly-in-string */ + ' msg = `hello ${name}', + 'line two ${name}', + 'and ${name} three`', + /* eslint-enable no-template-curly-in-string */ + ' print msg + name', + 'end sub' + )); + program.validate(); + const expected = [ + loc(file.srcPath, 1, 4, 1, 8), + loc(file.srcPath, 2, 19, 2, 23), + loc(file.srcPath, 3, 11, 3, 15), + loc(file.srcPath, 4, 6, 4, 10), + loc(file.srcPath, 5, 16, 5, 20) + ]; + //inside `${}` on later lines of the template string + expect(referencesAt(file.srcPath, 3, 13)).to.eql(expected); + expect(referencesAt(file.srcPath, 4, 10)).to.eql(expected); + //after the template string + expect(referencesAt(file.srcPath, 5, 20)).to.eql(expected); + + //print msg| + expect(referencesAt(file.srcPath, 5, 13)).to.eql([ + loc(file.srcPath, 2, 4, 2, 7), + loc(file.srcPath, 5, 10, 5, 13) + ]); + }); + + it('finds items on the last line of a file with no trailing newline', () => { + const file = program.setFile('source/main.brs', lines('\n', + 'sub main()', + 'end sub', + 'sub last(): value = 1: print value: end sub' + )); + program.validate(); + const expected = [ + loc(file.srcPath, 2, 12, 2, 17), + loc(file.srcPath, 2, 29, 2, 34) + ]; + //print value|: + expect(referencesAt(file.srcPath, 2, 34)).to.eql(expected); + //|value = 1 + expect(referencesAt(file.srcPath, 2, 12)).to.eql(expected); + }); + + it('finds symbols in namespaces, classes, and empty function bodies', () => { + const file = program.setFile('source/main.bs', lines('\n', + 'namespace alpha.beta', + ' function noop()', + ' end function', + ' class Widget', + ' sub render()', + ' count = 1', + ' print count', + ' end sub', + ' end class', + ' sub caller()', + ' noop()', + ' end sub', + 'end namespace' + )); + program.validate(); + //variables in a class method inside a namespace + const expected = [ + loc(file.srcPath, 5, 12, 5, 17), + loc(file.srcPath, 6, 18, 6, 23) + ]; + expect(referencesAt(file.srcPath, 6, 23)).to.eql(expected); + expect(referencesAt(file.srcPath, 5, 12)).to.eql(expected); + + //call to an empty function from inside a namespace + expect(referencesAt(file.srcPath, 10, 12)).to.eql([ + loc(file.srcPath, 10, 8, 10, 12) + ]); + }); + }); + function locationToString(loc: Location) { return `${URI.parse(loc.uri).fsPath}:${loc.range.start.line}:${loc.range.start.character}-${loc.range.end.line}:${loc.range.end.character}`; } diff --git a/src/bscPlugin/selectionRanges/SelectionRangesProcessor.spec.ts b/src/bscPlugin/selectionRanges/SelectionRangesProcessor.spec.ts index fe88f32b0..5e1f8b94d 100644 --- a/src/bscPlugin/selectionRanges/SelectionRangesProcessor.spec.ts +++ b/src/bscPlugin/selectionRanges/SelectionRangesProcessor.spec.ts @@ -352,4 +352,381 @@ describe('SelectionRangesProcessor', () => { const hasCustom = ranges.some(r => r.range.start.line === 0 && r.range.end.character === 10); expect(hasCustom).to.be.true; }); + + describe('location boundaries', () => { + type RangeTuple = [number, number, number, number]; + + /** + * Get the full selection range chain (innermost to outermost) for each position, requesting each position individually + */ + function getChains(pkgPath: string, code: string, positions: Array<[number, number]>) { + const file = program.setFile(pkgPath, code); + program.validate(); + return positions.map(([line, character]) => { + const ranges = program.getSelectionRanges(file.srcPath, [util.createPosition(line, character)]); + expect(ranges).to.have.length(1, `expected exactly one selection range for position ${line}:${character}`); + return flatten(ranges[0]); + }); + } + + const loopsCode = [ + 'sub main()', + ' value = 1', + ' while value < 10', + ' value = value + 1', + ' if value = 5', + ' exitwhile', + ' end if', + ' end while', + ' for i = 0 to 3', + ' print i', + ' end for', + 'end sub', + 'sub empty()', + 'end sub' + ].join('\n'); + + it('computes exact chains at identifier boundaries, column 0, and inside loops with exitwhile', () => { + const [atStart, afterEnd, column0, onExit, afterExit, inFor, onIf] = getChains('source/main.brs', loopsCode, [ + [1, 4], // |value = 1 + [1, 9], // value| = 1 + [2, 0], // | while value < 10 + [5, 12], // |exitwhile + [5, 16], // exit|while + [9, 14], // print |i + [4, 8] // |if value = 5 + ]); + const outer: RangeTuple[] = [ + [1, 4, 10, 11], // function body block + [0, 0, 11, 7], // sub main()...end sub + [0, 0, 13, 7] // whole file + ]; + const whileChain: RangeTuple[] = [ + [4, 8, 6, 14], // if value = 5 ... end if + [3, 8, 6, 14], // while body block + [2, 4, 7, 13], // while ... end while + ...outer + ]; + expect(atStart).to.eql([ + [1, 4, 1, 9], // value + [1, 4, 1, 13], // value = 1 + ...outer + ]); + expect(afterEnd).to.eql(atStart); + expect(column0).to.eql(outer); + expect(onExit).to.eql([ + [5, 12, 5, 16], // exit (split from `exitwhile`) + [5, 12, 5, 21], // exitwhile + ...whileChain + ]); + expect(afterExit).to.eql(onExit); + expect(inFor).to.eql([ + [9, 14, 9, 15], // i + [9, 8, 9, 15], // print i + [8, 4, 10, 11], // for ... end for + ...outer + ]); + expect(onIf).to.eql([ + [4, 8, 4, 10], // if + ...whileChain + ]); + }); + + it('computes exact chains inside an empty function body followed by a column-0 `end sub`', () => { + const [afterParen, atEnd] = getChains('source/main.brs', loopsCode, [ + [12, 11], // sub empty()| + [13, 0] // |end sub + ]); + expect(afterParen).to.eql([ + [12, 10, 12, 11], // ) + [12, 11, 13, 0], // empty body block (from the end of `)` to the start of `end sub`) + [12, 0, 13, 7], // sub empty()...end sub + [0, 0, 13, 7] // whole file + ]); + expect(atEnd).to.eql([ + [13, 0, 13, 7], // end sub + [12, 11, 13, 0], // empty body block + [12, 0, 13, 7], // sub empty()...end sub + [0, 0, 13, 7] // whole file + ]); + }); + + it('computes exact chains for empty-bodied constructs and empty if/else/while/for blocks', () => { + const code = [ + 'namespace alpha', + 'end namespace', + 'class Beta', + 'end class', + 'interface Charlie', + 'end interface', + 'enum Delta', + 'end enum', + 'sub main()', + ' if true then', + ' else if false then', + ' else', + ' end if', + ' while true', + ' end while', + ' for i = 0 to 1', + ' end for', + 'end sub' + ].join('\n'); + const chains = getChains('source/main.bs', code, [ + [1, 0], // |end namespace + [3, 0], // |end class + [5, 0], // |end interface + [7, 0], // |end enum + [9, 16], // if true then| + [10, 0], // | else if false then + [11, 0], // | else + [11, 8], // else| + [12, 0], // | end if + [13, 14], // while true| + [14, 0], // | end while + [16, 0] // | end for + ]); + const file: RangeTuple = [0, 0, 17, 7]; + const main: RangeTuple[] = [ + [9, 4, 16, 11], // function body block + [8, 0, 17, 7], // sub main()...end sub + file + ]; + const ifChain: RangeTuple[] = [ + [9, 4, 12, 10], // if ... end if + ...main + ]; + const elseIfChain: RangeTuple[] = [ + [10, 9, 12, 10], // the nested `if false then ... end if` statement + ...ifChain + ]; + expect(chains).to.eql([ + [[1, 0, 1, 13], [0, 0, 1, 13], file], + [[3, 0, 3, 9], [2, 0, 3, 9], file], + [[5, 0, 5, 13], [4, 0, 5, 13], file], + [[7, 0, 7, 8], [6, 0, 7, 8], file], + // `then` token, then the empty `then` block (end of `then` to the start of `else`) + [[9, 12, 9, 16], [9, 16, 10, 4], ...ifChain], + [[9, 16, 10, 4], ...ifChain], + // the empty `else if` block (end of `then` to the start of `else`) + [[10, 22, 11, 4], ...elseIfChain], + // `else` token, then the empty `else` block (end of `else` to the start of `end if`) + [[11, 4, 11, 8], [11, 8, 12, 4], ...elseIfChain], + [[11, 8, 12, 4], ...elseIfChain], + // `true` token, then while statement + [[13, 10, 13, 14], [13, 4, 14, 13], ...main], + // empty while block + [[13, 14, 14, 4], [13, 4, 14, 13], ...main], + // empty for block + [[15, 18, 16, 4], [15, 4, 16, 11], ...main] + ]); + }); + + it('computes exact chains in a .brs file with CRLF line endings, emoji, and elseif', () => { + const code = [ + '\' 😀 header comment', + 'function alpha(a, b)', + ' if a then', + ' print "😀"', + ' elseif b then', + ' print 2', + ' else', + ' print 3', + ' end if', + 'end function', + 'sub beta() : print "😀😀" : end sub : sub charlie() : end sub', + 'sub empty()', + 'end sub', + '' + ].join('\r\n'); + const [onPrint, onElse, onElseIf, inElseIf, onEndSub, onCharlie, column0, afterParen, atEnd] = getChains('source/main.brs', code, [ + [3, 8], // |print "😀" + [4, 4], // |elseif b then + [4, 8], // else|if b then + [5, 8], // |print 2 + [10, 30], // en|d sub (after the emoji string) + [10, 42], // sub |charlie() (after the emoji string) + [2, 0], // | if a then + [11, 11], // sub empty()| + [12, 0] // |end sub + ]); + const body: RangeTuple = [1, 0, 12, 7]; + const alpha: RangeTuple[] = [ + [2, 4, 8, 10], // if ... end if + [1, 0, 9, 12], // function alpha...end function + body + ]; + expect(onPrint).to.eql([ + [3, 8, 3, 13], // print + [3, 8, 3, 18], // print "😀" + ...alpha + ]); + expect(onElse).to.eql([ + [4, 4, 4, 8], // else (split from `elseif`) + ...alpha + ]); + expect(onElseIf).to.eql([ + [4, 4, 4, 8], // else (split from `elseif`) + [4, 8, 8, 10], // if b then ... end if + ...alpha + ]); + expect(inElseIf).to.eql([ + [5, 8, 5, 13], // print + [5, 8, 5, 15], // print 2 + [4, 8, 8, 10], // if b then ... end if + ...alpha + ]); + expect(onEndSub).to.eql([ + [10, 28, 10, 35], // end sub + [10, 0, 10, 35], // sub beta() : print "😀😀" : end sub + body + ]); + expect(onCharlie).to.eql([ + [10, 42, 10, 49], // charlie + [10, 38, 10, 61], // sub charlie() : end sub + body + ]); + expect(column0).to.eql([ + [1, 0, 9, 12], // function alpha...end function + body + ]); + expect(afterParen).to.eql([ + [11, 10, 11, 11], // ) + [11, 11, 12, 0], // empty body block (from the end of `)` to the start of `end sub`) + [11, 0, 12, 7], // sub empty()...end sub + body + ]); + expect(atEnd).to.eql([ + [12, 0, 12, 7], // end sub + [11, 11, 12, 0], // empty body block + [11, 0, 12, 7], // sub empty()...end sub + body + ]); + }); + + it('uses utf-16 code units for positions that follow an emoji on the same line', () => { + const code = [ + 'const A = "😀" : const B = 2', + 'enum E', + ' x = "😀😀" : y = "b"', + 'end enum' + ].join('\n'); + const chains = getChains('source/main.bs', code, [ + [0, 22], // const| B = 2 + [0, 23], // const |B = 2 + [2, 17], // |y = "b" + [2, 24] // y = "b"| + ]); + expect(chains).to.eql([ + [[0, 17, 0, 22], [0, 17, 0, 28], [0, 0, 3, 8]], + [[0, 23, 0, 24], [0, 17, 0, 28], [0, 0, 3, 8]], + [[2, 17, 2, 18], [2, 17, 2, 24], [1, 0, 3, 8], [0, 0, 3, 8]], + [[2, 21, 2, 24], [2, 17, 2, 24], [1, 0, 3, 8], [0, 0, 3, 8]] + ]); + }); + + it('computes exact chains inside and after a multi-line template string', () => { + const code = [ + 'sub main(who as string)', + ' count = 1', + ' text = `line one 😀', + // eslint-disable-next-line no-template-curly-in-string + 'two ${who} and ${count}', + // eslint-disable-next-line no-template-curly-in-string + 'three ${ lcase(who) }`', + ' print text', + 'end sub', + 'function after()', + 'end function', + '' + ].join('\n'); + const chains = getChains('source/main.bs', code, [ + [3, 6], // two ${|who} + [4, 9], // three ${ |lcase(who) } + [4, 15], // three ${ lcase(|who) } + [7, 9] // function |after() + ]); + const template: RangeTuple[] = [ + [2, 11, 4, 22], // the whole template string + [2, 4, 4, 22], // text = `...` + [1, 4, 5, 14], // function body block + [0, 0, 6, 7], // sub main...end sub + [0, 0, 8, 12] // whole file + ]; + expect(chains).to.eql([ + [[3, 4, 3, 6], [3, 6, 3, 9], ...template], + [[4, 9, 4, 14], [4, 9, 4, 19], ...template], + [[4, 14, 4, 15], [4, 15, 4, 18], [4, 9, 4, 19], ...template], + [[7, 9, 7, 14], [7, 0, 8, 12], [0, 0, 8, 12]] + ]); + }); + + it('computes exact chains for nested namespaces and a symbol on the last line with no trailing newline', () => { + const code = [ + 'namespace alpha', + ' namespace beta.charlie', + ' class Person', + ' name as string', + ' sub speak()', + ' end sub', + ' end class', + ' end namespace', + ' enum Direction', + ' up = "up"', + ' down = "down"', + ' end enum', + ' interface Shape', + ' width as integer', + ' function area() as float', + ' end interface', + ' const PI = 3.14', + 'end namespace', + 'function empty()', + 'end function', + 'const LAST_ONE = true' + ].join('\n'); + const chains = getChains('source/main.bs', code, [ + [20, 6], // const |LAST_ONE = true + [20, 21], // const LAST_ONE = true| + [16, 10], // const |PI = 3.14 + [1, 19], // namespace beta.|charlie + [9, 13], // up = |"up" + [18, 16], // function empty()| + [19, 0] // |end function + ]); + const alphaBody: RangeTuple[] = [ + [1, 4, 16, 19], // namespace alpha body + [0, 0, 17, 13], // namespace alpha...end namespace + [0, 0, 20, 21] // whole file + ]; + expect(chains).to.eql([ + [[20, 6, 20, 14], [20, 0, 20, 21], [0, 0, 20, 21]], + [[20, 17, 20, 21], [20, 0, 20, 21], [0, 0, 20, 21]], + [[16, 10, 16, 12], [16, 4, 16, 19], ...alphaBody], + [[1, 18, 1, 19], [1, 14, 1, 26], [1, 4, 7, 17], ...alphaBody], + [[9, 13, 9, 17], [9, 8, 9, 17], [8, 4, 11, 12], ...alphaBody], + // `)`, then the empty body block (end of `)` to the start of `end function`) + [[18, 15, 18, 16], [18, 16, 19, 0], [18, 0, 19, 12], [0, 0, 20, 21]], + [[19, 0, 19, 12], [18, 16, 19, 0], [18, 0, 19, 12], [0, 0, 20, 21]] + ]); + }); + + it('returns the same chains when all positions are requested in a single call', () => { + const file = program.setFile('source/main.brs', loopsCode); + program.validate(); + const positions = [ + util.createPosition(1, 4), + util.createPosition(5, 12), + util.createPosition(9, 14) + ]; + const combined = program.getSelectionRanges(file.srcPath, positions).map(x => flatten(x)); + const individual = positions.map(x => flatten(program.getSelectionRanges(file.srcPath, [x])[0])); + expect(combined).to.eql(individual); + expect(combined.map(x => x[0])).to.eql([ + [1, 4, 1, 9], + [5, 12, 5, 16], + [9, 14, 9, 15] + ]); + }); + }); }); diff --git a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts index cdc656012..85614a758 100644 --- a/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts +++ b/src/bscPlugin/semanticTokens/BrsFileSemanticTokensProcessor.spec.ts @@ -593,4 +593,176 @@ describe('BrsFileSemanticTokensProcessor', () => { [SemanticTokenTypes.function, 2, 16, 2, 20] ]); }); + + describe('location boundaries', () => { + it('computes exact token ranges for nested multi-line constructs in a .bs file with no trailing newline', () => { + const file = program.setFile('source/main.bs', [ + 'namespace alpha', + ' namespace beta.charlie', + ' class Person', + ' name as string', + ' sub speak()', + ' end sub', + ' end class', + ' end namespace', + ' enum Direction', + ' up = "up"', + ' down = "down"', + ' end enum', + ' interface Shape', + ' width as integer', + ' function area() as float', + ' end interface', + ' const PI = 3.14', + 'end namespace', + 'function empty()', + 'end function', + 'const LAST_ONE = true' + ].join('\n')); + expectSemanticTokens(file, [ + // namespace |alpha| + [SemanticTokenTypes.namespace, 0, 10, 0, 15], + // class |Person| + [SemanticTokenTypes.class, 2, 14, 2, 20], + // enum |Direction| + [SemanticTokenTypes.enum, 8, 9, 8, 18], + // interface |Shape| + [SemanticTokenTypes.interface, 12, 14, 12, 19], + // const |PI| = 3.14 + [SemanticTokenTypes.variable, 16, 10, 16, 12, [SemanticTokenModifiers.readonly, SemanticTokenModifiers.static]], + // function |empty|() + [SemanticTokenTypes.function, 18, 9, 18, 14], + // const |LAST_ONE| = true + [SemanticTokenTypes.variable, 20, 6, 20, 14, [SemanticTokenModifiers.readonly, SemanticTokenModifiers.static]] + ]); + }); + + it('computes exact token ranges in a .brs file with CRLF line endings, emoji, and elseif', () => { + const file = program.setFile('source/main.brs', [ + '\' 😀 header comment', + 'function alpha(a, b)', + ' if a then', + ' print "😀"', + ' elseif b then', + ' print 2', + ' else', + ' print 3', + ' end if', + 'end function', + 'sub beta() : print "😀😀" : end sub : sub charlie() : end sub', + 'sub empty()', + 'end sub', + '' + ].join('\r\n')); + expectSemanticTokens(file, [ + // function |alpha|(a, b) + [SemanticTokenTypes.function, 1, 9, 1, 14], + // function alpha(|a|, b) + [SemanticTokenTypes.parameter, 1, 15, 1, 16], + // function alpha(a, |b|) + [SemanticTokenTypes.parameter, 1, 18, 1, 19], + // sub |beta|() : print "😀😀" : end sub : sub charlie() : end sub + [SemanticTokenTypes.function, 10, 4, 10, 8], + // sub beta() : print "😀😀" : end sub : sub |charlie|() : end sub + [SemanticTokenTypes.function, 10, 42, 10, 49], + // sub |empty|() + [SemanticTokenTypes.function, 11, 4, 11, 9] + ]); + }); + + it('uses utf-16 code units for tokens that follow an emoji on the same line', () => { + const file = program.setFile('source/main.bs', [ + 'const A = "😀" : const B = 2', + 'enum E', + ' x = "😀😀" : y = "b"', + 'end enum' + ].join('\n')); + expectSemanticTokens(file, [ + // const |A| = "😀" : const B = 2 + [SemanticTokenTypes.variable, 0, 6, 0, 7, [SemanticTokenModifiers.readonly, SemanticTokenModifiers.static]], + // const A = "😀" : const |B| = 2 + [SemanticTokenTypes.variable, 0, 23, 0, 24, [SemanticTokenModifiers.readonly, SemanticTokenModifiers.static]], + // enum |E| + [SemanticTokenTypes.enum, 1, 5, 1, 6] + ]); + }); + + it('computes exact token ranges inside and after a multi-line template string', () => { + const file = program.setFile('source/main.bs', [ + 'sub main(who as string)', + ' count = 1', + ' text = `line one 😀', + // eslint-disable-next-line no-template-curly-in-string + 'two ${who} and ${count}', + // eslint-disable-next-line no-template-curly-in-string + 'three ${ lcase(who) }`', + ' print text', + 'end sub', + 'function after()', + 'end function', + '' + ].join('\n')); + expectSemanticTokens(file, [ + // sub |main|(who as string) + [SemanticTokenTypes.function, 0, 4, 0, 8], + // sub main(|who| as string) + [SemanticTokenTypes.parameter, 0, 9, 0, 12], + // |count| = 1 + [SemanticTokenTypes.variable, 1, 4, 1, 9], + // |text| = `line one 😀 + [SemanticTokenTypes.variable, 2, 4, 2, 8], + // two ${|who|} and ${count} + [SemanticTokenTypes.variable, 3, 6, 3, 9], + // two ${who} and ${|count|} + [SemanticTokenTypes.variable, 3, 17, 3, 22], + // three ${ |lcase|(who) }` + [SemanticTokenTypes.function, 4, 9, 4, 14], + // three ${ lcase(|who|) }` + [SemanticTokenTypes.variable, 4, 15, 4, 18], + // print |text| + [SemanticTokenTypes.variable, 5, 10, 5, 14], + // function |after|() + [SemanticTokenTypes.function, 7, 9, 7, 14] + ]); + }); + + it('computes exact token ranges in loops, exitwhile, and an empty function at the end of a .brs file', () => { + const file = program.setFile('source/main.brs', [ + 'sub main()', + ' value = 1', + ' while value < 10', + ' value = value + 1', + ' if value = 5', + ' exitwhile', + ' end if', + ' end while', + ' for i = 0 to 3', + ' print i', + ' end for', + 'end sub', + 'sub empty()', + 'end sub' + ].join('\n')); + expectSemanticTokens(file, [ + // sub |main|() + [SemanticTokenTypes.function, 0, 4, 0, 8], + // |value| = 1 + [SemanticTokenTypes.variable, 1, 4, 1, 9], + // while |value| < 10 + [SemanticTokenTypes.variable, 2, 10, 2, 15], + // |value| = value + 1 + [SemanticTokenTypes.variable, 3, 8, 3, 13], + // value = |value| + 1 + [SemanticTokenTypes.variable, 3, 16, 3, 21], + // if |value| = 5 + [SemanticTokenTypes.variable, 4, 11, 4, 16], + // for |i| = 0 to 3 + [SemanticTokenTypes.variable, 8, 8, 8, 9], + // print |i| + [SemanticTokenTypes.variable, 9, 14, 9, 15], + // sub |empty|() + [SemanticTokenTypes.function, 12, 4, 12, 9] + ]); + }); + }); }); diff --git a/src/bscPlugin/symbols/DocumentSymbolProcessor.spec.ts b/src/bscPlugin/symbols/DocumentSymbolProcessor.spec.ts index 07b8cdb8e..994dabacf 100644 --- a/src/bscPlugin/symbols/DocumentSymbolProcessor.spec.ts +++ b/src/bscPlugin/symbols/DocumentSymbolProcessor.spec.ts @@ -5,6 +5,7 @@ import { rootDir } from '../../testHelpers.spec'; import type { DocumentSymbol } from 'vscode-languageserver-types'; import { SymbolKind } from 'vscode-languageserver-types'; import type { BrsFile } from '../../files/BrsFile'; +import { util } from '../../util'; let sinon = createSandbox(); describe('DocumentSymbolProcessor', () => { @@ -288,6 +289,157 @@ describe('DocumentSymbolProcessor', () => { }); }); + describe('location boundaries', () => { + /** + * Flatten the document symbols into `name|kind|range|selectionRange` strings, indenting children by 2 spaces per level + */ + function getSymbolStrings(srcPath: string) { + const result: string[] = []; + function walk(symbols: DocumentSymbol[], indent: string) { + for (const symbol of symbols ?? []) { + result.push(`${indent}${symbol.name}|${SymbolKindMap.get(symbol.kind)}|${util.rangeToString(symbol.range)}|${util.rangeToString(symbol.selectionRange)}`); + walk(symbol.children, indent + ' '); + } + } + walk(program.getDocumentSymbols(srcPath), ''); + return result; + } + + it('computes exact ranges for nested multi-line constructs in a .bs file with no trailing newline', () => { + program.setFile('source/main.bs', [ + 'namespace alpha', + ' namespace beta.charlie', + ' class Person', + ' name as string', + ' sub speak()', + ' end sub', + ' end class', + ' end namespace', + ' enum Direction', + ' up = "up"', + ' down = "down"', + ' end enum', + ' interface Shape', + ' width as integer', + ' function area() as float', + ' end interface', + ' const PI = 3.14', + 'end namespace', + 'function empty()', + 'end function', + 'const LAST_ONE = true' + ].join('\n')); + expect(getSymbolStrings('source/main.bs')).to.eql([ + 'alpha|Namespace|0:0-17:13|0:10-0:15', + ' charlie|Namespace|1:4-7:17|1:14-1:26', + ' Person|Class|2:8-6:17|2:14-2:20', + ' name|Field|3:12-3:26|3:12-3:16', + ' speak|Method|4:12-5:19|4:16-4:21', + ' Direction|Enum|8:4-11:12|8:9-8:18', + ' up|EnumMember|9:8-9:17|9:8-9:10', + ' down|EnumMember|10:8-10:21|10:8-10:12', + ' Shape|Interface|12:4-15:17|12:14-12:19', + ' width|Field|13:8-13:24|13:8-13:13', + ' area|Method|14:8-14:32|14:17-14:21', + ' PI|Constant|16:4-16:19|16:10-16:12', + 'empty|Function|18:0-19:12|18:9-18:14', + 'LAST_ONE|Constant|20:0-20:21|20:6-20:14' + ]); + }); + + it('computes exact ranges for empty-bodied constructs', () => { + program.setFile('source/main.bs', [ + 'namespace alpha', + 'end namespace', + 'class Beta', + 'end class', + 'interface Charlie', + 'end interface', + 'enum Delta', + 'end enum', + 'sub main()', + ' if true then', + ' else if false then', + ' else', + ' end if', + ' while true', + ' end while', + ' for i = 0 to 1', + ' end for', + 'end sub' + ].join('\n')); + expect(getSymbolStrings('source/main.bs')).to.eql([ + 'alpha|Namespace|0:0-1:13|0:10-0:15', + 'Beta|Class|2:0-3:9|2:6-2:10', + 'Charlie|Interface|4:0-5:13|4:10-4:17', + 'Delta|Enum|6:0-7:8|6:5-6:10', + 'main|Function|8:0-17:7|8:4-8:8' + ]); + }); + + it('computes exact ranges in a .brs file with CRLF line endings, emoji, and elseif', () => { + program.setFile('source/main.brs', [ + '\' 😀 header comment', + 'function alpha(a, b)', + ' if a then', + ' print "😀"', + ' elseif b then', + ' print 2', + ' else', + ' print 3', + ' end if', + 'end function', + 'sub beta() : print "😀😀" : end sub : sub charlie() : end sub', + 'sub empty()', + 'end sub', + '' + ].join('\r\n')); + expect(getSymbolStrings('source/main.brs')).to.eql([ + 'alpha|Function|1:0-9:12|1:9-1:14', + 'beta|Function|10:0-10:35|10:4-10:8', + 'charlie|Function|10:38-10:61|10:42-10:49', + 'empty|Function|11:0-12:7|11:4-11:9' + ]); + }); + + it('uses utf-16 code units for symbols that follow an emoji on the same line', () => { + program.setFile('source/main.bs', [ + 'const A = "😀" : const B = 2', + 'enum E', + ' x = "😀😀" : y = "b"', + 'end enum' + ].join('\n')); + expect(getSymbolStrings('source/main.bs')).to.eql([ + 'A|Constant|0:0-0:14|0:6-0:7', + 'B|Constant|0:17-0:28|0:23-0:24', + 'E|Enum|1:0-3:8|1:5-1:6', + ' x|EnumMember|2:4-2:14|2:4-2:5', + ' y|EnumMember|2:17-2:24|2:17-2:18' + ]); + }); + + it('computes exact ranges for symbols after a multi-line template string', () => { + program.setFile('source/main.bs', [ + 'sub main(who as string)', + ' count = 1', + ' text = `line one 😀', + // eslint-disable-next-line no-template-curly-in-string + 'two ${who} and ${count}', + // eslint-disable-next-line no-template-curly-in-string + 'three ${ lcase(who) }`', + ' print text', + 'end sub', + 'function after()', + 'end function', + '' + ].join('\n')); + expect(getSymbolStrings('source/main.bs')).to.eql([ + 'main|Function|0:0-6:7|0:4-0:8', + 'after|Function|7:0-8:12|7:9-7:14' + ]); + }); + }); + function expectSymbols(documentSymbols: DocumentSymbol[], expected: SymbolTree) { expect( symbolKindToString(createSymbolTree(documentSymbols)) diff --git a/src/bscPlugin/symbols/WorkspaceSymbolProcessor.spec.ts b/src/bscPlugin/symbols/WorkspaceSymbolProcessor.spec.ts index 2142cc102..7f4480453 100644 --- a/src/bscPlugin/symbols/WorkspaceSymbolProcessor.spec.ts +++ b/src/bscPlugin/symbols/WorkspaceSymbolProcessor.spec.ts @@ -279,4 +279,120 @@ describe('WorkspaceSymbolProcessor', () => { ['g', SymbolKind.EnumMember] ]); }); + + describe('location boundaries', () => { + /** + * Get every workspace symbol as a `name|kind|containerName|uri|range` string, sorted for stable comparison + */ + function getSymbolStrings() { + return program.getWorkspaceSymbols().map((x: any) => { + return `${x.name}|${SymbolKindMap.get(x.kind)}|${x.containerName}|${x.location.uri}|${util.rangeToString(x.location.range)}`; + }).sort(); + } + + function uri(pkgPath: string) { + return util.pathToUri(s`${rootDir}/${pkgPath}`); + } + + it('computes exact locations across .bs and .brs files, including CRLF, emoji, and nested namespaces', () => { + program.setFile('source/main.bs', [ + 'namespace alpha', + ' namespace beta.charlie', + ' class Person', + ' name as string', + ' sub speak()', + ' end sub', + ' end class', + ' end namespace', + ' enum Direction', + ' up = "up"', + ' down = "down"', + ' end enum', + ' interface Shape', + ' width as integer', + ' function area() as float', + ' end interface', + ' const PI = 3.14', + 'end namespace', + 'function empty()', + 'end function', + 'const LAST_ONE = true' + ].join('\n')); + program.setFile('source/lib.brs', [ + '\' 😀 header comment', + 'function alpha(a, b)', + ' if a then', + ' print "😀"', + ' elseif b then', + ' print 2', + ' end if', + 'end function', + 'sub beta() : print "😀😀" : end sub : sub charlie() : end sub', + 'sub emptyBrs()', + 'end sub', + '' + ].join('\r\n')); + const main = uri('source/main.bs'); + const lib = uri('source/lib.brs'); + expect(getSymbolStrings()).to.eql([ + `alpha|Function|undefined|${lib}|1:9-1:14`, + `alpha|Namespace|undefined|${main}|0:10-0:15`, + `area|Method|Shape|${main}|14:17-14:21`, + `beta|Function|undefined|${lib}|8:4-8:8`, + `charlie|Function|undefined|${lib}|8:42-8:49`, + `charlie|Namespace|alpha|${main}|1:14-1:26`, + `Direction|Enum|alpha|${main}|8:9-8:18`, + `down|EnumMember|Direction|${main}|10:8-10:12`, + `empty|Function|undefined|${main}|18:9-18:14`, + `emptyBrs|Function|undefined|${lib}|9:4-9:12`, + `LAST_ONE|Constant|undefined|${main}|20:6-20:14`, + `name|Field|Person|${main}|3:12-3:16`, + `Person|Class|charlie|${main}|2:14-2:20`, + `PI|Constant|alpha|${main}|16:10-16:12`, + `Shape|Interface|alpha|${main}|12:14-12:19`, + `speak|Method|Person|${main}|4:16-4:21`, + `up|EnumMember|Direction|${main}|9:8-9:10`, + `width|Field|Shape|${main}|13:8-13:13` + ].sort()); + }); + + it('uses utf-16 code units for symbols that follow an emoji on the same line', () => { + program.setFile('source/main.bs', [ + 'const A = "😀" : const B = 2', + 'enum E', + ' x = "😀😀" : y = "b"', + 'end enum' + ].join('\n')); + const main = uri('source/main.bs'); + expect(getSymbolStrings()).to.eql([ + `A|Constant|undefined|${main}|0:6-0:7`, + `B|Constant|undefined|${main}|0:23-0:24`, + `E|Enum|undefined|${main}|1:5-1:6`, + `x|EnumMember|E|${main}|2:4-2:5`, + `y|EnumMember|E|${main}|2:17-2:18` + ].sort()); + }); + + it('computes exact locations for symbols after a multi-line template string', () => { + program.setFile('source/main.bs', [ + 'sub main(who as string)', + ' count = 1', + ' text = `line one 😀', + // eslint-disable-next-line no-template-curly-in-string + 'two ${who} and ${count}', + // eslint-disable-next-line no-template-curly-in-string + 'three ${ lcase(who) }`', + ' print text', + 'end sub', + 'function after()', + 'end function', + '' + ].join('\n')); + const main = uri('source/main.bs'); + expect(getSymbolStrings()).to.eql([ + `after|Function|undefined|${main}|7:9-7:14`, + `main|Function|undefined|${main}|0:4-0:8` + ].sort()); + }); + }); }); diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts index 2297ef4f4..4bdddfad3 100644 --- a/src/bscPlugin/validation/ScopeValidator.spec.ts +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -18,6 +18,8 @@ import { ScopeValidator } from './ScopeValidator'; import type { FunctionStatement, ReturnStatement } from '../../parser/Statement'; import { Logger } from '@rokucommunity/logger'; import { ParseMode } from '../..'; +import { Range } from 'vscode-languageserver'; +import { util, standardizePath as s } from '../../util'; describe('ScopeValidator', () => { @@ -7392,4 +7394,325 @@ describe('ScopeValidator', () => { expect((unvalidatedSegments[0] as FunctionStatement).getName(ParseMode.BrighterScript)).to.equal('doStuff'); }); }); + + /* eslint-disable no-template-curly-in-string */ + describe('location boundaries', () => { + function uriOf(pkgPath: string) { + return util.pathToUri(s`${rootDir}/${pkgPath}`); + } + + function range(startLine: number, startChar: number, endLine: number, endChar: number) { + return Range.create(startLine, startChar, endLine, endChar); + } + + it('flags an identifier in a .brs file', () => { + program.setFile('source/main.brs', 'sub main()\n print notDefined\nend sub\n'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 10, 1, 20) + } + }]); + }); + + it('flags an identifier in a .bs file', () => { + program.setFile('source/main.bs', 'sub main()\n print notDefined\nend sub\n'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(1, 10, 1, 20) + } + }]); + }); + + it('flags a token at column 0 right after a newline', () => { + program.setFile('source/main.brs', 'sub main()\nnotDefined()\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindFunction('notDefined').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 0, 1, 10) + } + }]); + }); + + it('flags a token at the end of a line and at the end of a file with no trailing newline', () => { + program.setFile('source/main.brs', 'sub main()\nend sub\nprint notDefined'); + program.validate(); + expectDiagnostics(program, [{ + //the whole statement (starting at column 0) + code: DiagnosticMessages.unexpectedStatementOutsideFunction().code, + location: { + uri: uriOf('source/main.brs'), + range: range(2, 0, 2, 16) + } + }, { + //the very last token in the file + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.brs'), + range: range(2, 6, 2, 16) + } + }]); + }); + + it('flags a multi-line statement in a .brs file', () => { + program.setFile('source/main.brs', 'sub main()\n return {\n a: 1\n }\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + //the whole return statement + code: DiagnosticMessages.voidFunctionMayNotReturnValue('sub').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 4, 3, 5) + } + }, { + //just the returned expression + code: DiagnosticMessages.returnTypeMismatch('roAssociativeArray', 'void').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 11, 3, 5) + } + }]); + }); + + it('flags a multi-line statement in a .bs file', () => { + program.setFile('source/main.bs', 'sub main()\n x as integer = {\n a: 1\n }\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.assignmentTypeMismatch('roAssociativeArray', 'integer').code, + location: { + uri: uriOf('source/main.bs'), + range: range(1, 4, 3, 5) + } + }]); + }); + + it('flags a multi-line statement in a file with CRLF line endings', () => { + program.setFile('source/main.brs', 'sub main()\r\n return {\r\n a: 1\r\n }\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.voidFunctionMayNotReturnValue('sub').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 4, 3, 5) + } + }, { + code: DiagnosticMessages.returnTypeMismatch('roAssociativeArray', 'void').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 11, 3, 5) + } + }]); + }); + + it('flags identifiers in a file with CRLF line endings', () => { + program.setFile('source/main.bs', 'sub main()\r\n a = 1\r\n print notDefined\r\nnotDefined2()\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(2, 10, 2, 20) + } + }, { + code: DiagnosticMessages.cannotFindFunction('notDefined2').code, + location: { + uri: uriOf('source/main.bs'), + range: range(3, 0, 3, 11) + } + }]); + }); + + it('flags identifiers in a file mixing LF and CRLF line endings', () => { + program.setFile('source/main.brs', 'sub main()\r\n a = 1\n print notDefined\r\n\n\r\n print a + notDefined2\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.brs'), + range: range(2, 10, 2, 20) + } + }, { + code: DiagnosticMessages.cannotFindName('notDefined2').code, + location: { + uri: uriOf('source/main.brs'), + range: range(5, 14, 5, 25) + } + }]); + }); + + it('counts a surrogate-pair emoji before the flagged token as two characters', () => { + program.setFile('source/main.brs', 'sub main()\n print "😀" + notDefined\n print "😀😀" + notDefined2\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.brs'), + range: range(1, 17, 1, 27) + } + }, { + code: DiagnosticMessages.cannotFindName('notDefined2').code, + location: { + uri: uriOf('source/main.brs'), + range: range(2, 19, 2, 30) + } + }]); + }); + + it('counts a surrogate-pair emoji before the flagged token on a CRLF line', () => { + program.setFile('source/main.bs', 'sub main()\r\n print "😀" + notDefined\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(1, 17, 1, 27) + } + }]); + }); + + it('flags a token after a multi-line template string', () => { + program.setFile('source/main.bs', 'sub main()\n x = `a\n${1}\n`: print notDefined\n print notDefined2\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(3, 9, 3, 19) + } + }, { + code: DiagnosticMessages.cannotFindName('notDefined2').code, + location: { + uri: uriOf('source/main.bs'), + range: range(4, 10, 4, 21) + } + }]); + }); + + it('flags a token after a multi-line template string with CRLF line endings', () => { + program.setFile('source/main.bs', 'sub main()\r\n x = `a\r\n${1}\r\n`: print notDefined\r\n print notDefined2\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(3, 9, 3, 19) + } + }, { + code: DiagnosticMessages.cannotFindName('notDefined2').code, + location: { + uri: uriOf('source/main.bs'), + range: range(4, 10, 4, 21) + } + }]); + }); + + it('flags a token inside a template string expression on a later line of the template', () => { + program.setFile('source/main.bs', 'sub main()\n x = `😀 ${1}\n and ${notDefined}\n`\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(2, 8, 2, 18) + } + }]); + }); + + it('flags a token inside a template string expression on a later CRLF line of the template', () => { + program.setFile('source/main.bs', 'sub main()\r\n x = `😀 ${1}\r\n and ${notDefined}\r\n`\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.cannotFindName('notDefined').code, + location: { + uri: uriOf('source/main.bs'), + range: range(2, 8, 2, 18) + } + }]); + }); + + it('includes correct related information uris and ranges across files', () => { + program.setFile('source/a.brs', 'sub main()\nend sub\n\nsub doIt()\nend sub'); + program.setFile('source/b.brs', 'sub doIt()\r\nend sub'); + program.validate(); + expectDiagnostics(program, [{ + code: DiagnosticMessages.duplicateFunctionImplementation('doIt').code, + location: { + uri: uriOf('source/a.brs'), + range: range(3, 4, 3, 8) + }, + relatedInformation: [{ + location: { + uri: uriOf('source/b.brs'), + range: range(0, 4, 0, 8) + }, + message: 'Function declared here' + }, { + location: { + uri: uriOf('source/a.brs'), + range: range(3, 4, 3, 8) + }, + message: `In scope 'source'` + }] + }, { + code: DiagnosticMessages.duplicateFunctionImplementation('doIt').code, + location: { + uri: uriOf('source/b.brs'), + range: range(0, 4, 0, 8) + }, + relatedInformation: [{ + location: { + uri: uriOf('source/a.brs'), + range: range(3, 4, 3, 8) + }, + message: 'Function declared here' + }, { + location: { + uri: uriOf('source/b.brs'), + range: range(0, 4, 0, 8) + }, + message: `In scope 'source'` + }] + }]); + }); + + it('includes correct locations for xml diagnostics in a CRLF file', () => { + program.setFile('components/Comp.xml', '\r\n\r\n