UNPKG

@bscotch/gml-parser

Version:

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

156 lines 6.62 kB
import { arrayWrapped } from '@bscotch/utility'; import { parseFeatherTypeString, } from './jsdoc.feather.js'; import { Type } from './types.js'; import { primitiveNames } from './types.primitives.js'; import { ok } from './util.js'; function findInKnownOrGenerics(identifier, knownTypes) { const collections = arrayWrapped(knownTypes); for (const collection of collections) { let found; if (collection instanceof Map) { found = collection.get(identifier); } else { found = collection[identifier]; } if (found) { return arrayWrapped(found); } } return; } export function typeFromFeatherString(typeString, knownTypes, addMissing) { const parsed = parseFeatherTypeString(typeString); return typeFromParsedFeatherString(parsed, knownTypes, addMissing); } /** * Given a type identifier, get a parsed Type instance. Useful for * the "leaves" of a type tree, e.g. "String" or "Struct.Mystruct". * Only creates primitive types, e.g. "Struct.MyStruct" will return * a plain `Type<"Struct">` instance. * * When knownTypes are provided, will return a known type by exact * identifier match if it exists. Otherwise a new type instance will * be created *and added to the knownTypes map*. */ export function typeFromIdentifier(identifier, knownTypes, addMissing, __isRootRequest = true) { ok(identifier.match(/^[A-Z][A-Z0-9._]*$/i), `Invalid type name ${identifier}`); const normalizedName = identifier?.toLocaleLowerCase?.(); const isObjectType = ['asset.gmobject', 'id.instance'].includes(normalizedName); const knownType = findInKnownOrGenerics(identifier, knownTypes)?.[0]; if (knownType && isObjectType) { // Need a derived type to prevent mutation of the parent! return knownType.derive(); } else if (knownType) { return knownType; } const primitiveType = primitiveNames.find((n) => n?.toLocaleLowerCase?.() === normalizedName); if (primitiveType) { return new Type(primitiveType); } if (identifier.match(/\./)) { // Then we might still be able to get a base type. const [baseType, ...nameParts] = identifier.split('.'); const type = typeFromIdentifier(baseType, knownTypes, addMissing, false); if (__isRootRequest && type) { // Then add to the known types map const derivedTyped = type.derive().named(nameParts.join('.')); // Types should only be auto-added when loading the native spec if (addMissing) { const map = arrayWrapped(knownTypes).find((collection) => collection instanceof Map); map?.set(identifier, derivedTyped); } return derivedTyped; } return type; } return new Type('Undefined'); } export function typeFromParsedJsdocs(jsdoc, knownTypes, addMissing) { if (jsdoc.kind === 'description') { // Then we have no type info. return []; } else if (['type', 'instancevar', 'globalvar', 'localvar'].includes(jsdoc.kind)) { // Then this was purely a type annotation. Create the type and // add any metadata. return typeFromFeatherString(jsdoc.type?.content || 'Any', knownTypes, addMissing); } else if (jsdoc.kind === 'self') { // The self-type could be a function, in which case // we want to use its "self" as the type instead of // the function itself. const matchingType = typeFromFeatherString(jsdoc.self?.content || 'Any', knownTypes, addMissing) .map((t) => (t.kind === 'Function' ? t.self : t)) .filter((x) => !!x); return matchingType; } else if (jsdoc.kind === 'function') { const type = new Type('Function').describe(jsdoc.description); let i = 0; const generics = {}; for (const generic of jsdoc.templates || []) { generics[generic.name.content] = typeFromFeatherString(generic.type?.content || 'Any', knownTypes, addMissing); // Flag them all as generic, named after the generic name for (const genericType of generics[generic.name.content]) { genericType.isGeneric = true; genericType.named(generic.name.content); } } if (jsdoc.self) { type.self = typeFromFeatherString(jsdoc.self.content, knownTypes, addMissing)[0]; } if (jsdoc.returns && jsdoc.returns.type) { const returnType = typeFromFeatherString(jsdoc.returns.type.content, [knownTypes, generics], addMissing); type.addReturnType(returnType); } for (const param of jsdoc.params || []) { const member = type.addParameter(i, param.name.content); if (param.type) { member.setType(typeFromFeatherString(param.type.content, [knownTypes, generics], addMissing)); } i++; member.optional = !!param.optional; member.describe(param.description); } return [type]; } return []; } export function typeFromParsedFeatherString(node, knownTypes, addMissing) { if (node.kind === 'type') { const identifier = node.name; let type = typeFromIdentifier(identifier.content, knownTypes, addMissing); if (node.of) { const subtypes = typeFromParsedFeatherString(node.of, knownTypes, addMissing); // Then we need to create a new type instead of mutating // the one we found. type = type.derive(); // While only some types are "containers", we can // go ahead and add any contained types and worry // about the consequences later. That way we don't // need to keep updating this as we add more container // types. for (const subtype of subtypes) { type.addItemType(subtype); } // TODO: Else create a diagnostic? } return [type]; } else if (node.kind === 'union') { const unionOf = node.types; if (unionOf.length === 1) { return typeFromParsedFeatherString(unionOf[0], knownTypes, addMissing); } const types = []; for (const child of unionOf) { const subtype = typeFromParsedFeatherString(child, knownTypes, addMissing); types.push(...subtype); } return types; } throw new Error(`Unknown node type ${node['name']}`); } //# sourceMappingURL=types.feather.js.map