UNPKG

@bscotch/gml-parser

Version:

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

271 lines 11.2 kB
import { parseJsdoc } from './jsdoc.js'; import { logger } from './logger.js'; import { GmlVisitorBase } from './parser.js'; import { identifierFrom } from './parser.utility.js'; import { Diagnostic } from './project.diagnostics.js'; import { Range } from './project.location.js'; import { Signifier } from './signifiers.js'; import { typeFromParsedJsdocs } from './types.feather.js'; import { Type } from './types.js'; import { StitchParserError, assert } from './util.js'; export function registerGlobals(file) { try { const processor = new GlobalDeclarationsProcessor(file); const visitor = new GmlGlobalDeclarationsVisitor(processor); visitor.EXTRACT_GLOBAL_DECLARATIONS(file.cst); } catch (parseError) { const err = new StitchParserError(`Error parsing globals in ${file.path}`); err.cause = parseError; logger.error(err); } } class GlobalDeclarationsProcessor { file; localScopeStack = []; start; constructor(file) { this.file = file; this.localScopeStack.push(file.scopes[0].local); assert(file.scopes[0], 'File must have a global scope'); this.start = file.scopes[0].start; } range(loc) { return Range.fromCst(this.start.file, loc); } get currentLocalScope() { return this.localScopeStack.at(-1); } pushLocalScope() { const localScope = new Type('Struct'); this.localScopeStack.push(localScope); } popLocalScope() { this.localScopeStack.pop(); } get asset() { return this.file.asset; } get project() { return this.asset.project; } get globalSelf() { return this.project.self; } } /** * Visits the CST and creates symbols for global signifiers. */ export class GmlGlobalDeclarationsVisitor extends GmlVisitorBase { PROCESSOR; static validated = false; EXTRACT_GLOBAL_DECLARATIONS(input) { this.PROCESSOR.file.callsSuper = false; // If we are reprocessing, we want the list of globals that used // to be declared here in case any have gone missing. this.visit(input); return this.PROCESSOR; } /** * Register a global identifier from its declaration. Note that * global identifiers are not deleted when their definitions are, * so we need to either create *or update* the corresponding symbol/typeMember. */ REGISTER_GLOBAL(children) { const name = children.Identifier?.[0]; if (!name) return; const range = this.PROCESSOR.range(name); return this.REGISTER_GLOBAL_BY_NAME(name.image, range); } REGISTER_GLOBAL_BY_NAME(name, range, isNotDef = false) { // Create it if it doesn't already exist. let symbol = this.PROCESSOR.globalSelf.getMember(name); if (!symbol) { symbol = new Signifier(this.PROCESSOR.project.self, name); // Add the symbol and type to the project. this.PROCESSOR.globalSelf.addMember(symbol); } // Ensure it's defined here. if (!isNotDef && !symbol.native) { symbol.definedAt(range); } else if (!isNotDef) { this.PROCESSOR.file.addDiagnostic('INVALID_OPERATION', new Diagnostic(`"${name}" already exists as a built-in symbol.`, range, 'warning')); } symbol.addRef(range, !isNotDef && !symbol.native); symbol.global = true; symbol.macro = false; // Reset macro status symbol.enum = false; // Reset enum status return symbol; } REGISTER_JSDOC_GLOBAL(jsdoc) { if (jsdoc.kind !== 'globalvar') { return; } const symbol = this.REGISTER_GLOBAL_BY_NAME(jsdoc.name.content, Range.from(this.PROCESSOR.file, jsdoc.name)); symbol.setType(typeFromParsedJsdocs(jsdoc, this.PROCESSOR.project.types, false)); symbol.describe(jsdoc.description); // NOTE: references to types are added during local processing so they are not needed here } jsdocJs(children) { this.REGISTER_JSDOC_GLOBAL(parseJsdoc(children.JsdocJs[0])); } jsdocGml(children) { for (const line of children.JsdocGmlLine) { this.REGISTER_JSDOC_GLOBAL(parseJsdoc(line)); } } /** * Collect the enum symbol *and* its members, since all of those * are globally visible. */ enumStatement(children) { const symbol = this.REGISTER_GLOBAL(children); assert(symbol, 'Enum symbol should exist'); symbol.enum = true; let type = symbol.getTypeByKind('Enum'); if (!type) { symbol.setType(new Type('Enum')); type = symbol.getTypeByKind('Enum'); } if (symbol.type.type.length > 1) { symbol.setType(type); } type.named(symbol.name); type.signifier = symbol; this.PROCESSOR.project.types.set(`Enum.${symbol.name}`, type); // Upsert the enum members for (let i = 0; i < children.enumMember.length; i++) { const name = children.enumMember[i].children.Identifier[0]; const range = this.PROCESSOR.range(name); // Does member already exist? const member = type.getMember(name.image) || type.addMember(name.image); const memberType = member.type.type[0] || new Type('EnumMember').named(name.image); member.setType(memberType); memberType.signifier = member; member.enumMember = true; member.idx = i; member.definedAt(range); member.addRef(range, true); } } /** * Identify global function declarations and store them as * symbols or `global.` types. For constructors, add the * corresponding types. */ functionExpression(children) { const isGlobal = this.PROCESSOR.currentLocalScope === this.PROCESSOR.file.scopes[0].local && this.PROCESSOR.asset.assetKind === 'scripts'; // Functions create a new localscope. Keeping track of that is important // for making sure that we're looking at a global function declaration. this.PROCESSOR.pushLocalScope(); const name = children.Identifier?.[0]; if (name && isGlobal) { // Add the function to a table of functions const constructorNode = children.constructorSuffix?.[0]; let parentConstructs; if (constructorNode?.children.Identifier) { // Ensure that the parent type exists const parentName = constructorNode.children.Identifier[0]?.image; if (parentName) { const parentNameRange = this.PROCESSOR.range(constructorNode.children.Identifier[0]); const parentSignifier = this.REGISTER_GLOBAL_BY_NAME(parentName, parentNameRange, true); let parentType = parentSignifier.type.type[0]; if (!parentType) { parentType = new Type('Function').named(parentName); parentSignifier.setType(parentType); } // Ensure it has a constructs type parentType.isConstructor = true; parentConstructs = parentType.self || new Type('Struct').named(parentName); parentType.self = parentConstructs; parentConstructs.signifier = parentSignifier; this.PROCESSOR.project.types.set(`Struct.${parentName}`, parentConstructs); } } const signifier = this.REGISTER_GLOBAL(children); // Make sure that the types all exist let type = signifier.getTypeByKind('Function'); if (!type) { // Create the type if needed, but if there's already an existing // type with this name grab that instead (helps reduce reference // problems during editing) const typeName = `Function.${name.image}`; type = (this.PROCESSOR.project.types.get(typeName) || new Type('Function').named(name.image)); signifier.setType(type); type = signifier.getTypeByKind('Function'); this.PROCESSOR.project.types.set(typeName, type); } // Global functions can only have one type! if (signifier.type.type.length > 1) { signifier.setType(type); } // Reset the self context to account for the user changing a function // from a constructor to a regular function and vice versa type.self = undefined; // Ensure that the type links back to the signifier type.signifier = signifier; // If it's a constructor, ensure the type exists if (constructorNode) { type.self = (this.PROCESSOR.project.types.get(`Struct.${name.image}`) || new Type('Struct').named(name.image)); type.self.signifier = signifier; this.PROCESSOR.project.types.set(`Struct.${name.image}`, type.self); } if (parentConstructs && type.self) { type.self.extends = parentConstructs; } // Initialize the local scope type.local ||= Type.Struct; } this.visit(children.blockStatement); // End the scope this.PROCESSOR.popLocalScope(); } globalVarDeclaration(children) { this.REGISTER_GLOBAL(children); } macroStatement(children) { const symbol = this.REGISTER_GLOBAL(children); symbol.macro = true; } identifierAccessor(children) { // Add global.whatever symbols const identifier = identifierFrom(children); if (identifier?.type === 'Global') { const globalIdentifier = children.accessorSuffixes?.[0].children.dotAccessSuffix?.[0].children .identifier[0].children; if (globalIdentifier?.Identifier) { this.REGISTER_GLOBAL(globalIdentifier); } } else if (identifier?.type === 'Identifier' && children.accessorSuffixes?.[0].children.functionArguments) { // See if this is a function call for `event_inherited()` if (identifier.name === 'event_inherited') { this.PROCESSOR.file.callsSuper = true; } } // Still visit the rest if (children.accessorSuffixes) { this.visit(children.accessorSuffixes); } } constructor(PROCESSOR) { super(); this.PROCESSOR = PROCESSOR; if (!GmlGlobalDeclarationsVisitor.validated) { // Validator logic only needs to run once, since // new instances will be the same. this.validateVisitor(); GmlGlobalDeclarationsVisitor.validated = true; } } } //# sourceMappingURL=visitor.globals.js.map