@bscotch/gml-parser
Version:
A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.
219 lines • 6.93 kB
JavaScript
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