UNPKG

@bscotch/gml-parser

Version:

A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.

219 lines 6.93 kB
import { assert } from './util.js'; export const firstLineIndex = 1; export const firstColumnIndex = 1; /** * Single-character tokens do not have correct end * location information. This function checks the `image` * of the token and fixes the end location if necessary. */ export function fixITokenLocation(token) { assert(typeof token.image === 'string', 'Token must have an image'); const length = token.image.length; if (token.endOffset === token.startOffset) { token.endOffset += length; } if (token.endColumn === token.startColumn) { token.endColumn += length; } return token; } export class Position { file; offset; line; column; $tag = 'Pos'; constructor(file, offset, line, column) { this.file = file; this.offset = offset; this.line = line; this.column = column; } /** * Create a new Positiong instance within this same file * at the given location. */ at(loc) { return Position.fromCstStart(this.file, loc); } /** * Create a new location starting at the end of * the given location. */ atEnd(loc) { return Position.fromCstEnd(this.file, loc); } equals(other) { return Position.equals(this, other); } static from(file, loc, fromTokenEnd = false) { if (loc instanceof Position) { return loc; } if ('offset' in loc) { return new Position(file, loc.offset, loc.line, loc.column); } return fromTokenEnd ? Position.fromCstEnd(file, loc) : Position.fromCstStart(file, loc); } static fromFileStart(fileName) { return new Position(fileName, 0, firstLineIndex, firstColumnIndex); } static fromCstStart(fileName, location) { return new Position(fileName, location.startOffset ?? 0, location.startLine ?? firstLineIndex, location.startColumn ?? firstColumnIndex); } static fromCstEnd(fileName, location) { return new Position(fileName, location.endOffset ?? 0, location.endLine ?? firstLineIndex, location.endColumn ?? firstColumnIndex); } static equals(a, b) { return a.file === b.file && a.offset === b.offset; } } export function isRange(value) { return value instanceof Range; } export class Range { $tag = 'Range'; start; end; constructor(start, end) { // We can get into some weird cases when recovering // from parse errors, so do some checks with graceful // recovery. this.start = start; this.end = end ?? start; if (this.end.offset < this.start.offset) { this.end = this.start; } } get file() { return this.start.file; } static from(file, location) { if ('start' in location) { return new Range(Position.from(file, location.start), Position.from(file, location.end)); } return Range.fromCst(file, location); } static fromCst(fileName, location) { return new Range(Position.fromCstStart(fileName, location), Position.fromCstEnd(fileName, location)); } static equals(a, b) { return Position.equals(a.start, b.start) && Position.equals(a.end, b.end); } } /** * A code range corresponding with a specific function argument. * Useful for providing signature help. */ export class FunctionArgRange extends Range { type; idx; $tag = 'ArgRange'; hasExpression = false; constructor( /** The function reference this call belongs to */ type, /** The index of the parameter we're in. */ idx, start, end) { super(start, end); this.type = type; this.idx = idx; } get param() { assert(this.type, 'FunctionArgRange must have a type'); return this.type.getParameter(this.idx); } } export class StructNewMemberRange extends Range { type; $tag = 'NewMemberRange'; constructor( /** The function reference this call belongs to */ type, start, end) { super(start, end); this.type = type; } } export class Scope extends Range { local; self; $tag = 'Scope'; /** The immediately adjacent ScopeRange */ _next = undefined; flags = 0; constructor(start, local, self) { super(start); this.local = local; this.self = self; } set isDotAccessor(value) { if (value) { this.flags |= 1 /* ScopeFlag.DotAccessor */; } else { this.flags &= ~1 /* ScopeFlag.DotAccessor */; } } get isDotAccessor() { return !!(this.flags & 1 /* ScopeFlag.DotAccessor */); } setEnd(atToken, fromTokenEnd = false) { this.end = Position.from(this.file, atToken, fromTokenEnd); } /** * Create the next ScopeRange, adjacent to this one. * This sets the end location of this scope range to * match the start location of the next one. The self * and local values default to the same as this scope range, * so at least one will need to be changed! */ createNext(atToken, fromTokenEnd = false) { assert(this.end, 'Cannot create a next scope range without an end to this one.'); assert(!this._next, 'Cannot create a next scope range when one already exists.'); const start = Position.from(this.file, atToken, fromTokenEnd); this._next = new Scope(start, this.local, this.self); return this._next; } } export class Reference extends Range { item; $tag = 'Ref'; /** If this is reference marks the declaration */ isDef = false; _itemNamePattern = undefined; constructor(item, start, end) { super(start, end); this.item = item; } get itemNamePattern() { if (!this._itemNamePattern) { this._itemNamePattern = new RegExp(`\\b(?<name>${this.item.name})\\b`); } return this._itemNamePattern; } /** * The text in this ref's range, which does not necessarily match * the text of the item it refers to (e.g. it could be `self` or similar) */ get text() { return this.start.file.content.slice(this.start.offset, this.end.offset + 1); } get isRenameable() { const text = this.text; return this.item.isRenameable && this.itemNamePattern.test(text); } /** * Get full text content of this reference if what it referenced were * to be renamed to the given name. This **does not** actually rename * the identifier! */ toRenamed(newName) { return this.text.replace(this.itemNamePattern, newName); } static fromRange(range, item) { assert(range, 'Cannot create a reference from an undefined range.'); return new Reference(item, range.start, range.end); } } //# sourceMappingURL=project.location.js.map