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