@bscotch/gml-parser
Version:
A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.
216 lines • 9.81 kB
JavaScript
import { withCtxKind } from './parser.js';
import { Range, fixITokenLocation } from './project.location.js';
import { Signifier } from './signifiers.js';
import { getTypeOfKind } from './types.checks.js';
import { typeFromFeatherString } from './types.feather.js';
import { Type, TypeStore } from './types.js';
import { withableTypes } from './types.primitives.js';
import { assert } from './util.js';
/** Visit a function's CST and update any signifiers and types */
export function visitFunctionExpression(children, ctx) {
const functionName = children.Identifier?.[0]?.image;
const docs = ctx.docs || this.PROCESSOR.consumeJsdoc();
ctx.docs = undefined;
const assignedTo = ctx.signifier;
ctx.signifier = undefined;
if (!children.blockStatement) {
// Then we're in a recovery situation and should just move along
return;
}
// Reset the list of return values
ctx = {
...ctx,
returns: [],
};
// Compute useful properties of this function to help figure out
// how to define its symbol, type, scope, etc.
const nameLocation = functionName
? this.PROCESSOR.range(children.Identifier[0])
: undefined;
const isConstructor = !!children.constructorSuffix;
const bodyLocation = children.blockStatement[0].location;
const isFunctionStatement = ctx.ctxKindStack.at(-1) === 'functionStatement';
const isMixin = !!docs?.jsdoc.mixin;
/** If this function has a corresponding signifier, either
* because it is a function declaration or because it is a
* function expression assigned to a variable, then that's
* what this is.
*
* @remarks Function expressions need not be assigned to anything, necessarily, so they may not have a signifier.
*/
let signifier;
if (assignedTo) {
signifier = assignedTo;
}
else if (isFunctionStatement && functionName) {
// Then this function is being created by declaration,
// without being assigned to a variable. Find or create
// the signifier and update its definedAt & refs.
const matching = this.FIND_ITEM(children, { excludeParents: true });
if (matching?.item.$tag === 'Sym') {
signifier = matching.item;
}
else {
signifier = new Signifier(this.PROCESSOR.currentSelf, functionName);
// This function is overriding any parent function of the same name
signifier.override = true;
this.PROCESSOR.currentSelf.addMember(signifier);
}
if (nameLocation && signifier && !signifier.def) {
signifier?.definedAt(nameLocation);
signifier?.addRef(nameLocation, true);
}
}
// Get or create the function type. Use the existing type if there is one.
signifier?.describe(docs?.jsdoc.description);
const functionType = signifier?.getTypeByKind('Function') ||
getTypeOfKind(ctx.type, 'Function')?.derive() ||
new Type('Function').named(functionName);
signifier?.setType(functionType);
if (signifier && docs?.jsdoc.deprecated) {
signifier.deprecated = true;
}
if (signifier && docs?.jsdoc.mixin) {
signifier.mixin = true;
}
functionType.isConstructor = isConstructor;
functionType.self = isConstructor
? functionType.self || this.PROCESSOR.createStruct(bodyLocation)
: undefined;
if (isConstructor) {
functionType.self?.named(functionName);
}
functionType.returns ||= new TypeStore();
// Determine the function context.
let docContextRaw = docs?.jsdoc.kind === 'self'
? docs.type[0]
: docs?.jsdoc.kind === 'function'
? docs.type[0]?.self
: undefined;
if (docContextRaw && docContextRaw.kind === 'Function') {
// Then we use the function's construct if it is a constructor, else its context.
docContextRaw = docContextRaw.self;
}
const docContext = getTypeOfKind(docContextRaw, withableTypes);
let context;
if (isConstructor) {
context = functionType.self;
}
else if (docContext) {
context = docContext;
}
else if (ctx.self) {
// Then we're inside of a method() call, and the self
// is from the prior argument, and we aren't overriding
// using jsdoc.
context = ctx.self;
}
else if (isMixin) {
// Then we want to use a new struct type as the context,
// allowing calls to this function to add those variables to themselves.
// Try to keep the old context if possible.
context =
functionType.self && functionType.self !== this.PROCESSOR.currentSelf
? functionType.self
: this.PROCESSOR.createStruct(bodyLocation);
}
ctx.self = undefined; // Just to make sure nothing downstream uses it
context ||= this.PROCESSOR.currentSelf;
functionType.self = context;
// Ensure local context
const currentLocalContext = this.PROCESSOR.currentLocalScope;
functionType.local ||= Type.Struct;
assert(functionType.local !== currentLocalContext, 'Function local context incorrectly set to prior local context.');
// Functions have their own localscope as well as their self scope,
// so we need to push both.
const startParen = fixITokenLocation(children.functionParameters[0].children.StartParen[0]);
this.PROCESSOR.scope.setEnd(startParen);
this.PROCESSOR.pushScope(startParen, functionType.self, functionType.local, true);
// Handle definitiveScope -- if this is a constructor or mixin,
// we want to push a new definitiveScope.
this.PROCESSOR.pushDefinitiveSelf(isConstructor || isMixin ? functionType.self : undefined);
// Add function signature components. Must take into account that we may
// be updating after an edit.
const cstParams = children.functionParameters?.[0]?.children.functionParameter || [];
let totalParams = 0;
for (let i = 0; i < cstParams.length; i++) {
const paramCtx = withCtxKind(ctx, 'functionParam');
const paramToken = cstParams[i].children.Identifier[0];
const name = paramToken.image;
const range = this.PROCESSOR.range(paramToken);
// Use JSDocs to determine the type, description, etc of the parameter
let fromJsdoc = docs?.type?.[0]?.local?.getMember(name);
if (fromJsdoc && paramToken.image !== fromJsdoc.name) {
this.PROCESSOR.addDiagnostic('JSDOC_MISMATCH', paramToken, `Parameter name mismatch`);
// Unset it so we don't accidentally use it!
fromJsdoc = undefined;
}
const paramDoc = fromJsdoc
? docs?.jsdoc.params?.find((p) => p.name?.content === name)
: undefined;
// Params are just local variables
let param = functionType.local.getMember(name);
param = functionType
.addParameter(i, param || name, {
optional: fromJsdoc?.optional || !!cstParams[i].children.Assign,
})
.definedAt(range);
param.describe(fromJsdoc?.description);
param.addRef(range, true);
let inferredType;
if (cstParams[i].children.assignmentRightHandSide) {
inferredType = this.assignmentRightHandSide(cstParams[i].children.assignmentRightHandSide[0].children, paramCtx);
}
const paramType = fromJsdoc?.type.type || inferredType || this.ANY;
param.setType(paramType);
// Add a reference to the jsdoc name
if (paramDoc?.name) {
param.addRef(Range.from(this.PROCESSOR.file, paramDoc.name));
}
totalParams++;
}
// If we have more args defined in JSDocs, add them as *undeclared* params
const docsParams = docs?.jsdoc.params;
if ((docsParams?.length || 0) > cstParams.length) {
const extraParams = docsParams.slice(cstParams.length);
assert(extraParams, 'Expected extra params');
for (let i = 0; i < extraParams.length; i++) {
const idx = cstParams.length + i;
const paramDoc = extraParams[i];
assert(paramDoc, 'Expected extra param');
const type = docs.type[0]?.local?.getMember(paramDoc.name.content)
?.type;
functionType
.addParameter(idx, paramDoc.name.content, {
optional: paramDoc.optional,
type: type?.type ||
typeFromFeatherString(paramDoc.type?.content || 'Any', this.PROCESSOR.project.types, false),
})
.describe(paramDoc.description);
totalParams++;
}
}
// Remove any excess parameters, e.g. if we're updating a
// prior definition. This is tricky since we may need to do something
// about references to legacy params.
functionType.truncateParameters(totalParams);
// Process the function body
this.visit(children.blockStatement, withCtxKind(ctx, 'functionBody'));
// Pop the definitiveScope
this.PROCESSOR.popDefinitiveSelf();
// Update the RETURN type based on the return statements found in the body
if (docs?.type[0]?.returns) {
functionType.setReturnType(docs.type[0].returns.type);
// TODO: Check against the inferred return types
}
else {
functionType.setReturnType(ctx.returns?.length ? ctx.returns : this.UNDEFINED);
}
// End the scope
const endBrace = fixITokenLocation(children.blockStatement[0].children.EndBrace[0]);
this.PROCESSOR.scope.setEnd(endBrace);
this.PROCESSOR.popScope(endBrace, true);
assert(functionType.local !== this.PROCESSOR.currentLocalScope, 'Local scope not popped correctly');
return functionType;
}
//# sourceMappingURL=visitor.functionExpression.js.map