UNPKG

@bscotch/gml-parser

Version:

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

381 lines 17.1 kB
import { arrayWrapped } from '@bscotch/utility'; import { withCtxKind } from './parser.js'; import { identifierFrom, isEmpty, rhsFrom, sortedAccessorSuffixes, sortedFunctionCallParts, } from './parser.utility.js'; import { FunctionArgRange, Position, Range, fixITokenLocation, } from './project.location.js'; import { getTypeOfKind, getTypeStoreOrType, getTypes, getTypesOfKind, isTypeOfKind, normalizeType, replaceGenerics, updateGenericsMap, } from './types.checks.js'; import { withableTypes } from './types.primitives.js'; import { assignVariable } from './visitor.assign.js'; export function visitIdentifierAccessor(children, ctx) { const docs = this.PROCESSOR.consumeJsdoc(); const suffixes = sortedAccessorSuffixes(children.accessorSuffixes); // Compute useful metadata /** If true, then the `new` keyword prefixes this. */ const usesNew = !!children.New?.length; /** If not `undefined`, this is the assignment node */ const rhs = rhsFrom(children.assignment); const scope = this.PROCESSOR.fullScope; // Use the starting identifier to construct the initial `lastAccessed` object const identifierName = identifierFrom(children.identifier[0].children)?.name; let lastAccessed = { name: identifierName, range: Range.fromCst(this.PROCESSOR.file, children.identifier[0].location), usesNew, ctx, }; // Early exit if we don't have a name, since that should only // happen in a broken state (e.g. during editing) if (!lastAccessed.name) return [this.ANY]; // See if this is a known identifier. If not, create it in an // "undeclared" state. let item = this.FIND_ITEM(children.identifier[0].children)?.item; if (!item && scope.self === scope.global) { // Then this is being treated as a global variable but it has // not been declared anywhere const autoDeclarePrefixes = this.PROCESSOR.project.options?.settings?.autoDeclareGlobalsPrefixes || []; const isAutoDeclared = autoDeclarePrefixes.some((prefix) => lastAccessed.name?.startsWith(prefix)); if (!isAutoDeclared) { this.PROCESSOR.addDiagnostic('UNDECLARED_GLOBAL_REFERENCE', lastAccessed.range, `${lastAccessed.name} looks like a global but is not declared anywhere.`); } // Just set the last accessed type to ANY so that we can // continue processing. lastAccessed.types = [this.ANY]; } else if (!item) { // Then this is a signifier that we have not seen declared yet, // but might be declared later. So add it to the self scope but // without setting where it's defined. Diagnostics should be added // later. item = scope.self.addMember(lastAccessed.name); if (item) { item.instance = true; } } // Update lastAccessed with the signifier info if (item?.$tag === 'Sym') { lastAccessed.signifier = item; lastAccessed.types = arrayWrapped(getTypeStoreOrType(item)); // Add a reference! But if this is an assignment that'll be // handled later. const refAddedLater = rhs && !item.def; if (!refAddedLater) { item.addRef(lastAccessed.range); } } else if (item?.$tag === 'Type') { lastAccessed.signifier = item.signifier; lastAccessed.types = [item]; } // Now that we have the initial "lastAccessed" content, // we can iterate over the accessors. for (let i = 0; i < suffixes.length; i++) { // If this is the final suffix, we need to pass additional // info that is applicable to it. if (i === suffixes.length - 1) { lastAccessed.rhs = rhs; lastAccessed.docs = docs; } lastAccessed = processNextAccessor(this, lastAccessed, suffixes[i], suffixes[i + 1]); } return lastAccessed.types || [this.ANY]; } /** * @param nextAccessor If there is another accessor after this one, having it as a lookahead is useful in some cases. */ function processNextAccessor(visitor, lastAccessed, accessor, nextAccessor) { // For example, `hello[expression]` if ('expression' in accessor.children) { visitor.visit(accessor.children.expression, lastAccessed.ctx); } let nextAccessed; // Many suffix cases include an expression we need to evaluate. switch (accessor.name) { case 'arrayMutationAccessorSuffix': case 'arrayAccessSuffix': case 'mapAccessSuffix': case 'gridAccessSuffix': case 'listAccessSuffix': case 'structAccessSuffix': // All of the above are container types, so we need to // return the type of the items in the container. First // restrict the incoming types to those that are supported // by the accessor type. const allowedTypes = getTypesOfKind(lastAccessed.types, accessor.name.startsWith('array') ? 'Array' : accessor.name === 'mapAccessSuffix' ? 'Id.DsMap' : accessor.name === 'gridAccessSuffix' ? 'Id.DsGrid' : accessor.name === 'listAccessSuffix' ? 'Id.DsList' : accessor.name === 'structAccessSuffix' ? 'Struct' : 'Any'); nextAccessed = { range: Range.fromCst(visitor.PROCESSOR.file, accessor.location), ctx: lastAccessed.ctx, }; nextAccessed.types = allowedTypes .map((t) => t.items) .filter((t) => !!t); // If there is a RHS, we can't create a variable from it but // do need to process it! if (lastAccessed.rhs) { visitor.assignmentRightHandSide(lastAccessed.rhs, lastAccessed.ctx); } break; case 'dotAccessSuffix': nextAccessed = processDotAccessor(visitor, lastAccessed, accessor, nextAccessor); break; case 'functionArguments': nextAccessed = processFunctionArguments(visitor, lastAccessed, accessor); break; } return nextAccessed; } function processFunctionArguments(visitor, lastAccessed, suffix) { const nextAccessed = { range: Range.fromCst(visitor.PROCESSOR.file, suffix.location), ctx: lastAccessed.ctx, }; // Get the first function type from the lastAccessed types const functionType = getTypeOfKind(lastAccessed.types, 'Function'); // If this is a mixin call, then we need to ensure that the context // includes the variables created by the mixin function. if ((lastAccessed.signifier?.mixin || functionType?.signifier?.mixin) && functionType?.self) { const variables = functionType.self; for (const member of variables.listMembers()) { if (!member.def) continue; member.override = true; // Ensure it's set as an override variable const currentMember = visitor.PROCESSOR.currentSelf.getMember(member.name); if (currentMember?.native) continue; visitor.PROCESSOR.currentSelf.addMember(member); } } /** * The native `method` function has the unique property * of causing its first argument to be used as the scope * for the second argument. */ const isMethodCall = functionType?.signifier === visitor.PROCESSOR.project.self.getMember('method'); let methodSelf; /** If this is a `method()` call, the 2nd argument is the return type */ let methodReturns; // Create the argumentRanges between the parens and each comma const argsAndSeps = sortedFunctionCallParts(suffix); let argIdx = 0; let lastDelimiter; let lastTokenWasDelimiter = true; const ranges = []; const generics = new Map(); for (let i = 0; i < argsAndSeps.length; i++) { const token = argsAndSeps[i]; const isSep = 'image' in token; if (isSep) { fixITokenLocation(token); if (token.image === '(') { lastDelimiter = token; continue; } // Otherwise create the range // For some reason the end position is the same // as the start position for the commas and parens // Start on the RIGHT side of the first delimiter if (functionType) { const start = Position.fromCstEnd(visitor.PROCESSOR.file, lastDelimiter); // end on the LEFT side of the second delimiter const end = Position.fromCstStart(visitor.PROCESSOR.file, token); const funcRange = new FunctionArgRange(functionType, argIdx, start, end); if (!lastTokenWasDelimiter) { funcRange.hasExpression = true; } visitor.PROCESSOR.file.addFunctionArgRange(funcRange); ranges.push(funcRange); } // Increment the argument idx for the next one lastDelimiter = token; lastTokenWasDelimiter = true; argIdx++; } else { lastTokenWasDelimiter = false; const functionCtx = withCtxKind(lastAccessed.ctx, 'functionArg'); if (isMethodCall && argIdx === 1 && methodSelf) { functionCtx.self = methodSelf; } const expectedType = functionType?.getParameter(argIdx); if (expectedType) { functionCtx.type = expectedType.type; } if (token.children.jsdoc) { visitor.jsdoc(token.children.jsdoc[0].children, functionCtx); } const inferredType = normalizeType(visitor.assignmentRightHandSide(token.children.assignmentRightHandSide[0].children, functionCtx), visitor.PROCESSOR.project.types); if (isMethodCall && argIdx === 1) { // Then the inferred argument type is the return type methodReturns = getTypeOfKind(inferredType, ['Function']); } if (expectedType) { updateGenericsMap(expectedType, inferredType, visitor.PROCESSOR.project.types, generics); } if (isMethodCall && argIdx === 0) { methodSelf = getTypeOfKind(inferredType, [ 'Id.Instance', 'Struct', 'Asset.GMObject', ]); } } } // The returntype of this function may be used in another accessor const returnType = normalizeType(replaceGenerics((lastAccessed.usesNew ? functionType?.self : isMethodCall ? methodReturns : functionType?.returns) || visitor.ANY, visitor.PROCESSOR.project.types, generics), visitor.PROCESSOR.project.types); nextAccessed.types = [returnType]; // Add the function call to the file for diagnostics if (ranges.length) { visitor.PROCESSOR.file.addFunctionCall(ranges); } return nextAccessed; } function processDotAccessor(visitor, lastAccessed, accessor, nextAccessor) { const nextAccessed = { range: Range.fromCst(visitor.PROCESSOR.file, accessor.location), ctx: lastAccessed.ctx, }; // Reduce the available types from lastAccessed to those that // are dot-accessible const dottableTypes = getTypesOfKind(lastAccessed.types, [ ...withableTypes, 'Enum', ]); if (!dottableTypes.length) { // Early return. Just set the type to ANY and move along. nextAccessed.types = [visitor.ANY]; const allTypes = getTypes(lastAccessed.types); const isDotAccessible = !allTypes.length || getTypeOfKind(allTypes, ['Any', 'Unknown', 'Mixed']); if (!isDotAccessible) { visitor.PROCESSOR.addDiagnostic('INVALID_OPERATION', accessor.location, `Type does not allow dot accessors.`); } lastAccessed.rhs && visitor.assignmentRightHandSide(lastAccessed.rhs, lastAccessed.ctx); return nextAccessed; } let dottableType = dottableTypes[0]; if (dottableTypes.length > 1) { // We may have a union of valid types, so it's helpful to know // the subsequent accessor (if there is one) -- we can use it // to narrow down which type is likely intended. const nextAccessorName = nextAccessor?.name === 'dotAccessSuffix' ? identifierFrom(nextAccessor.children.identifier)?.name : undefined; if (nextAccessorName) { dottableType = dottableTypes.find((t) => t.getMember(nextAccessorName)) || dottableType; } } // Then we need to change self-scope to be inside // the prior dot-accessible. const dotAccessor = accessor.children; const dot = fixITokenLocation(dotAccessor.Dot[0]); // Set the self-scope starting right after the dot operator visitor.PROCESSOR.scope.setEnd(dot); visitor.PROCESSOR.pushSelfScope(dot, dottableType, true, { accessorScope: true, }); let scopeIsPopped = false; const popSelfScope = (on = propertyNameLocation) => { if (scopeIsPopped) return; visitor.PROCESSOR.scope.setEnd(on, true); visitor.PROCESSOR.popSelfScope(on, true); scopeIsPopped = true; }; // While editing a user will dot into something // prior to actually adding the new identifier. // To provide autocomplete options, we need to // still add a scopeRange for the dot. const hasIdentifier = !isEmpty(dotAccessor.identifier[0].children); if (!hasIdentifier) { popSelfScope(dot); nextAccessed.types = [visitor.ANY]; return nextAccessed; } // Then we've got an identifier! Get its info. const propertyIdentifier = identifierFrom(dotAccessor); const propertyNameLocation = dotAccessor.identifier[0].location; const propertyNameRange = visitor.PROCESSOR.range(propertyNameLocation); nextAccessed.range = propertyNameRange; nextAccessed.name = propertyIdentifier.name; // If we have an existing property, we just need to add // a ref and update the nextAccessed info. If not, we'll // need to first create that property. let property = dottableType.getMember(propertyIdentifier.name); if (property) { // If there's an assignment and this isn't an enum, then // handle then offload to the assignment logic if (lastAccessed.rhs && !isTypeOfKind(dottableType, 'Enum')) { // Pop the self-scope so the RHS is in the right place! popSelfScope(); property = assignVariable(visitor, { name: propertyIdentifier.name, container: dottableType, range: propertyNameRange, }, lastAccessed.rhs, { ctx: lastAccessed.ctx, docs: lastAccessed.docs, instance: true, })?.item || property; } else { // Otherwise we'll need to manually add the reference property.addRef(propertyNameRange); } } else if (dottableType.kind === 'Enum') { // Then we're trying to get an enum member that doesn't exist! visitor.PROCESSOR.addDiagnostic('INVALID_OPERATION', accessor.location, `Undefined enum member.`); } else if (lastAccessed.rhs) { // Then this variable is not yet defined on this struct, // but we have an assignment operation to use to define it. // We need to pop the scope first so that the RHS is evaluated // in the correct scope! popSelfScope(); property = assignVariable(visitor, { name: propertyIdentifier.name, container: dottableType, range: propertyNameRange, }, lastAccessed.rhs, { ctx: lastAccessed.ctx, docs: lastAccessed.docs, instance: true, })?.item; } else { // Then this variable is not yet defined on this struct. // We need to add it! If this is an assignment operation, then // we can use the central assignment logic. Otherwise we just // need to add an "undeclared" variable to the current type. property = dottableType.addMember(propertyIdentifier.name); if (property) { property.instance = true; property.addRef(propertyNameRange); } else { visitor.PROCESSOR.addDiagnostic('INVALID_OPERATION', accessor.location, `Unknown property.`); } } popSelfScope(); nextAccessed.signifier = property; nextAccessed.types = arrayWrapped(property?.type || visitor.ANY); return nextAccessed; } //# sourceMappingURL=visitor.identifierAccessor.js.map