UNPKG

@bscotch/gml-parser

Version:

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

457 lines 15.5 kB
import { arrayWrapped } from '@bscotch/utility'; import { typeToFeatherString } from './jsdoc.feather.js'; import { Flags } from './signifiers.flags.js'; import { Signifier } from './signifiers.js'; import { getTypes, narrows } from './types.checks.js'; import { typeFromFeatherString, } from './types.feather.js'; import { typeToHoverDetails, typeToHoverText } from './types.hover.js'; import { assert, ok } from './util.js'; /** * A stable entity that represents a type. It should be used * as the referenced container for any type information, so * that the types can be changed within the container without * breaking references. */ export class TypeStore extends Flags { $tag = 'TypeStore'; _types = []; constructor() { super(); } /** If this store has only one type, its kind. Else throws. */ get kind() { if (this.type.length === 0) { return 'Undefined'; } else if (this.type.length > 1) { return 'Mixed'; } return this.type[0].kind; } get type() { return [...this._types]; } set type(types) { this._types = arrayWrapped(types); } get hasTypes() { return this._types.length > 0; } get constructs() { return this.type .map((t) => (t.isConstructor ? t.self : undefined)) .filter((x) => !!x); } get items() { return this.type.map((t) => t.items).filter((x) => !!x); } get returns() { return this.type.map((t) => t.returns).filter((x) => !!x); } /** * Should be used sparingly, since it means we're adding types in multiple steps instead of all at once. */ addType(type) { this.type = [...this.type, ...arrayWrapped(type)]; return this; } narrows(other) { return narrows(this, other); } toFeatherString() { const typeStrings = [ ...new Set(this.type.map((t) => t.toFeatherString())), ].sort((a, b) => a.localeCompare(b)); return typeStrings.join('|') || 'Any'; } } const typeFlags = { READONLY: 1 << 0, GENERIC: 1 << 1, CONSTRUCTOR: 1 << 2, }; export class Type { _kind; $tag = 'Type'; // Some types have names. It only counts as a name if it // cannot be parsed into types given the name alone. // E.g. `Array<String>` is not a name, but `Struct.MyStruct` // results in the name `MyStruct`. name = undefined; description = undefined; /** Signifiers associated with this type. */ _signifier = undefined; /** * If set, then this Type is treated as a subset of the parent. * It will only "match" another type if that type is in its * parent somewhere. Useful for struct/constructor inheritence, as well * as for e.g. representing a subset of Real constants in a type. */ _extends = undefined; _derived = undefined; flags = 0; /** Named members of Structs and Enums */ _members = undefined; /** Types of the items found in arrays and various ds types, or the fallback type found in Structs */ items = undefined; // Applicable to Functions /** * For functions, the local variables declared within the function. * A subset of these will be parameters, which are also signifiers. */ local = undefined; /** * If this is a constructor function, then this is the * type of the struct that it constructs. * Otherwise it's the self-context of the function */ self = undefined; returns = undefined; constructor(_kind) { this._kind = _kind; } setFlag(flag, value) { if (value) { this.flags |= flag; } else { this.flags &= ~flag; } } getFlag(flag) { return !!(this.flags & flag); } /** * Native and primitive types are typically read-only once * they've been defined. This property should be set once a type * is intended to be immutable. */ get isReadonly() { return this.getFlag(typeFlags.READONLY); } set isReadonly(value) { this.setFlag(typeFlags.READONLY, value); } /** * If this is a type used as a generic, then this will be true */ get isGeneric() { return this.getFlag(typeFlags.GENERIC); } set isGeneric(value) { this.setFlag(typeFlags.GENERIC, value); } get isConstructor() { return this.getFlag(typeFlags.CONSTRUCTOR); } set isConstructor(value) { this.setFlag(typeFlags.CONSTRUCTOR, value); } get kind() { return this._kind; } set kind(newKind) { ok(this._kind === 'Unknown' || this._kind === newKind, 'Cannot change type kind'); this._kind = newKind; } get signifier() { return this._signifier || this.extends?.signifier; } set signifier(signifier) { // assert(!this._signifier, 'Cannot change type signifier'); this._signifier = signifier; } get extends() { return this._extends; } set extends(type) { const oldParent = this._extends; this._extends = type; oldParent?._derived?.delete(this); if (this._extends) { this._extends._derived ||= new Set(); this._extends._derived.add(this); } } listDerived(recursive = false) { if (!this._derived) { return []; } const derived = [...this._derived]; if (recursive) { for (const child of this._derived) { derived.push(...child.listDerived(true)); } } return derived; } get canBeSelf() { return ['Struct', 'Id.Instance', 'Asset.GMObject'].includes(this.kind); } /** If this type narrows `other` type, returns `true` */ narrows(other) { return narrows(this, other); } /** Get this type as a Feather-compatible string */ toFeatherString() { return typeToFeatherString(this); } get code() { return typeToHoverText(this); } get details() { return typeToHoverDetails(this); } get isFunction() { return this.kind === 'Function'; } setReturnType(type) { this.returns ||= new TypeStore(); const types = getTypes(type); this.returns.type = types; return this; } /** Prefer `setReturnType` where possible */ addReturnType(type) { ok(this.isFunction, `Cannot add return type to ${this.kind}`); this.returns ||= new TypeStore(); this.returns.addType(type); return this; } listParameters() { // Get the subset of local members that are parameters, // and sort them by their index. const params = this.local ?.listMembers(true) .filter((m) => m.parameter && typeof m.idx === 'number') || []; // Instead of sorting by index, we want to guarantee that the index positions // *actually match*. const sorted = Array(params.length); for (const param of params) { sorted[param.idx] = param; } return sorted; } getParameter(nameOrIdx) { const params = this.listParameters(); if (typeof nameOrIdx === 'string') { return params.find((p) => p?.name === nameOrIdx); } return params[nameOrIdx]; } /** A parameter is a special type of local variable. */ addParameter(idx, nameOrParam, options) { assert(this.isFunction, `Cannot add param to ${this.kind} type`); const name = typeof nameOrParam === 'string' ? nameOrParam : nameOrParam.name; let param = this.local?.getMember(name, true); const existingAtThisIndex = this.getParameter(idx); // Create the signifier if we need to if (!param) { this.local ||= Type.Struct; param = this.local.addMember(nameOrParam); assert(param.parent === this.local, 'Param incorrectly added -- has the wrong parent'); } // Handle positional conflicts. If there is a param at this index already, // unset its index so that it doesn't conflict with this one. if (existingAtThisIndex && existingAtThisIndex !== param) { existingAtThisIndex.idx = undefined; } param.idx = idx; param.local = true; param.parameter = true; param.optional = options?.optional || name === '...'; if (options?.type) { param.setType(options.type); } return param; } truncateParameters(count) { this.listParameters().forEach((p) => { if (p?.idx !== undefined && p.idx >= count) { p.idx = undefined; } }); } totalMembers(excludeParents = false) { if (this.kind === 'Id.Instance' || this.kind === 'Asset.GMObject') { return this.extends?.totalMembers(excludeParents) || 0; } if (excludeParents || !this.extends) { return this._members?.size || 0; } return ((this._members?.size || 0) + this.extends.totalMembers(excludeParents)); } listMembers(excludeParents = false) { // Handle pass-through types if (this.kind === 'Id.Instance' || this.kind === 'Asset.GMObject') { return this.extends?.listMembers(excludeParents) || []; } const members = this._members?.values() || []; if (excludeParents || !this.extends) { return [...members]; } return [...members, ...this.extends.listMembers()]; } getMember(name, excludeParents = false) { // Handle pass-through types if (this.kind === 'Id.Instance' || this.kind === 'Asset.GMObject') { return this.extends?.getMember(name, excludeParents); } if (excludeParents) { return this._members?.get(name); } return this._members?.get(name) || this.extends?.getMember(name); } /** For container types that have named members, like Structs and Enums */ addMember(newMember, options) { // If this is a Id.Instance or Asset.GMObject type, then we want to add // the member to the parent Struct instead. if (this.kind === 'Id.Instance' || this.kind === 'Asset.GMObject') { return this.extends?.addMember(newMember, options); } // If this is an immutable type, then we can't add members to it. if (this.isReadonly) { return; } const type = options?.type; const name = typeof newMember === 'string' ? newMember : newMember.name; const signifierArg = typeof newMember === 'string' ? undefined : newMember; // Only add if this doesn't exist on *any parent* const existing = this.getMember(name, false); assert(!existing || !signifierArg || existing === signifierArg || signifierArg.override || options?.override, `Cannot replace existing member "${name}" with new member of the same name`); const existingOnThis = this.getMember(name, true); let member; if (signifierArg?.override || (signifierArg && options?.override)) { // Then we want to override the existing member member = signifierArg; } else { // Then we want to preferentially use the existing member member = existing || signifierArg; if (!member) { member = new Signifier(this, name); member.override = !!options?.override; member.writable = options?.writable ?? true; if (type) { member.setType(type); } } } if (member !== existing) { this._members ??= new Map(); // If the existing member has no def, then replace it // and transfer its refs if (existingOnThis) { for (const ref of existingOnThis.refs) { ref.item = member; ref.isDef = false; // Definition must come from rootmost member.refs.add(ref); ref.file.dirty = true; } } this._members.set(member.name, member); // Ensure that all children of this parent are referencing // the same root-most member. this.replaceMemberInChildren(member); } return member; } replaceMemberInChildren(member) { for (const child of this.listDerived()) { const toReplace = child._members?.get(member.name); if (toReplace?.override) { // Then we skip this and all descendents of it continue; } if (toReplace) { // Remove from the child child._members.delete(member.name); // Inherit its refs for (const ref of toReplace.refs) { ref.item = member; ref.isDef = false; // Definition must come from rootmost member.refs.add(ref); ref.file.dirty = true; } } // Continue down the tree child.replaceMemberInChildren(member); } } removeMember(name) { const member = this.getMember(name, true); if (!member) { return; } this._members.delete(name); // Flag all referencing files as dirty for (const ref of member.refs) { ref.file.dirty = true; } } /** * For container types that have non-named members, like arrays and DsTypes. * Can also be used for default Struct values. */ addItemType(type) { this.items ||= new TypeStore(); this.items.addType(type); return this; } /** * For container types that have non-named members, like arrays and DsTypes. * Can also be used for default Struct values. */ setItemType(type) { this.items ||= new TypeStore(); this.items.type = type; return this; } /** * Create a derived type: of the same kind, pointing to * this type as its parent. */ derive() { const derived = new Type(this.kind); derived.extends = this; derived.name = this.name; return derived; } named(name) { this.name = name; return this; } describe(description) { this.description = description; return this; } genericize() { this.isGeneric = true; return this; } /** Given a Feather-compatible type string, get a fully parsed type. */ static fromFeatherString(typeString, knownTypes, addMissing) { return typeFromFeatherString(typeString, knownTypes, addMissing); } static get Any() { return new Type('Any'); } static get Real() { return new Type('Real'); } static get String() { return new Type('String'); } static get Bool() { return new Type('Bool'); } static get Undefined() { return new Type('Undefined'); } static get Struct() { return new Type('Struct'); } static get Function() { return new Type('Function'); } } //# sourceMappingURL=types.js.map