UNPKG

@bscotch/gml-parser

Version:

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

633 lines 31.1 kB
import { expect } from 'chai'; import { logger } from './logger.js'; import { Project } from './project.js'; import { Native } from './project.native.js'; import { Signifier } from './signifiers.js'; import { resetSandbox } from './test.lib.js'; import { Type } from './types.js'; import { assert, ok } from './util.js'; describe('Project', function () { it('can load the GML spec', async function () { const spec = await Native.from(undefined, new Type('Struct'), new Map()); expect(spec).to.exist; // STRUCTS AND CONSTS const track = spec.types.get('Struct.Track'); ok(track); ok(track.kind === 'Struct'); const name = track.getMember('name'); ok(name); expect(name.type.kind).to.equal('String'); const visible = track.getMember('visible'); ok(visible); expect(visible.type.kind).to.equal('Bool'); const tracks = track.getMember('tracks'); ok(tracks); expect(tracks.type.kind).to.equal('Array'); expect(tracks.type.items[0].kind).to.equal('Struct'); expect(tracks.type.items[0].type[0]).to.eql(track); const keyframes = track.getMember('keyframes'); ok(keyframes); expect(keyframes.type.kind).to.equal('Array'); expect(keyframes.type.items[0].kind).to.equal('Struct'); expect(keyframes.type.items[0].type[0]).to.eql(spec.types.get('Struct.Keyframe')); const typeField = track.getMember('type'); ok(typeField); const expectedTypeType = spec.types.get('Constant.SequenceTrackType'); ok(expectedTypeType); ok(typeField.type.type[0] === expectedTypeType); ok(expectedTypeType.kind === 'Real'); // VARIABLES const depthSymbol = spec.globalSelf.getMember('depth'); ok(depthSymbol); expect(depthSymbol.type.kind).to.equal('Real'); // FUNCTIONS const scriptExecuteType = spec.types.get('Function.script_execute'); const scriptExecuteSymbol = spec.globalSelf.getMember('script_execute'); ok(scriptExecuteSymbol); ok(scriptExecuteSymbol.type.type[0] === scriptExecuteType); ok(scriptExecuteType); ok(scriptExecuteType.kind === 'Function'); expect(scriptExecuteType.listParameters()).to.have.lengthOf(2); expect(scriptExecuteType.listParameters()[0].name).to.equal('scr'); expect(scriptExecuteType.listParameters()[0].type.type).to.have.lengthOf(3); expect(scriptExecuteType.listParameters()[0].type.type[0].kind).to.equal('String'); expect(scriptExecuteType.listParameters()[0].type.type[1].kind).to.equal('Function'); expect(scriptExecuteType.listParameters()[0].type.type[2].kind).to.equal('Asset.GMScript'); expect(scriptExecuteType.listParameters()[1].name).to.equal('...'); }); it('can use fallback GmlSpec', async function () { await Project.fallbackGmlSpecPath.exists({ assert: true }); }); it('can analyze a representative project', async function () { const projectDir = 'samples/project'; const project = await Project.initialize(projectDir); ok(project); //#region ASSETS const script = project.getAssetByName('Script1'); const scriptFile = script.gmlFile; const jsdocs = project.getAssetByName('Jsdocs'); const jsdocsFile = jsdocs.gmlFile; const complexScript = project.getAssetByName('Complicated'); const complexScriptFile = complexScript.gmlFile; const recoveryScript = project.getAssetByName('Recovery'); const recoveryScriptFile = recoveryScript.gmlFile; const obj = project.getAssetByName('o_object'); const objCreate = obj.gmlFilesArray.find((f) => f.name === 'Create_0'); const objStep = obj.gmlFilesArray.find((f) => f.name === 'Step_0'); const parent = project.getAssetByName('o_parent'); const child = project.getAssetByName('o_child1'); const grandchild = project.getAssetByName('o_child1_child'); const child2 = project.getAssetByName('o_child2'); ok(script); ok(scriptFile); ok(jsdocs); ok(jsdocsFile); ok(complexScript); ok(complexScriptFile); ok(obj); ok(objCreate); ok(objStep); ok(recoveryScript); ok(recoveryScriptFile); ok(parent); ok(child); ok(grandchild); ok(child2); //#endregion ASSETS //#region OBJECT INHERITANCE // Child1 and Child2 both inherit from Parent // Child1Child inherits from Child1 // Child2 does not call event_inherited in its Create event const parentVars = ['parent_var']; const childVars = ['child1_var']; const grandchildVars = ['child1_child_var']; const child2Vars = ['child2_var']; // Check inheritance links ok(child.parent === parent); ok(grandchild.parent === child); ok(child2.parent === parent); // Check that variables are propery inherited expect(parent.instanceType?.listMembers().map((m) => m.name)).to.include.members(parentVars); expect(child.instanceType?.listMembers().map((m) => m.name)).to.include.members([...parentVars, ...childVars]); expect(grandchild.instanceType?.listMembers().map((m) => m.name)).to.include.members([...parentVars, ...childVars, ...grandchildVars]); expect(child2.instanceType?.listMembers().map((m) => m.name)).to.include.members(child2Vars); expect(child2.instanceType?.listMembers().map((m) => m.name)).not.to.include.members(parentVars); // Check that a reference to the parent_var works in the grandchild const grandChildRef = grandchild.gmlFile.getReferenceAt(4, 24); ok(grandChildRef); expect(grandChildRef.item.name).to.equal('parent_var'); ok(grandChildRef.item === parent.instanceType.getMember('parent_var')); ok(grandChildRef.item.def); //#endregion OBJECT INHERITANCE // Check fancy assignmnent operators const decrementer = script.gmlFile.getReferenceAt(72, 3); ok(decrementer); const decrementRhs = script.gmlFile.getReferenceAt(72, 19); ok(decrementRhs); // Check assignment to an unknown variable const unknownVar = script.gmlFile.getReferenceAt(76, 13); ok(!unknownVar); const unknownVarRhs = script.gmlFile.getReferenceAt(76, 24); ok(unknownVarRhs); // Check reference inside of an accessor const accessorRef = script.gmlFile.getReferenceAt(81, 27); ok(accessorRef); //#region GLOBALVARS const globalVarName = 'GLOBAL_SCRIPT_VAR'; const globalvarDef = scriptFile.getReferenceAt(2, 18); const globalvarRef = scriptFile.getReferenceAt(2, 37); const otherGlobalvarRef = objStep.getReferenceAt(2, 36); ok(globalvarDef); ok(globalvarRef); ok(otherGlobalvarRef); // All refs should point to the same thing ok(globalvarDef.item === globalvarRef.item); ok(globalvarDef.item === otherGlobalvarRef.item); // The definition should exist and be named const item = globalvarDef.item; ok(item.isRenameable); ok(item.def); ok(item.name === globalVarName); // The globalvar should have appropriate symbol and type info ok(item.$tag === 'Sym'); ok(item.global === true); //#endregion GLOBALVARS //#region REF RENAMING expect(globalvarRef.text).to.equal(globalVarName); expect(globalvarRef.isRenameable).to.equal(true); //#endregion REF RENAMING //#region ROOT SCRIPT SCOPE const inRootScriptScope = scriptFile.getInScopeSymbolsAt(762); ok(inRootScriptScope.length); // Local scope const localConstructed = inRootScriptScope.find((id) => id.name === 'const'); ok(localConstructed); ok(localConstructed.local); ok(!localConstructed.global); // Global scope const globalConstructor = inRootScriptScope.find((id) => id.name === 'GlobalConstructor'); ok(globalConstructor); ok(!globalConstructor.local); ok(globalConstructor.global); ok(globalConstructor.name === 'GlobalConstructor'); ok(globalConstructor.type.constructs); ok(globalConstructor.type.constructs[0].name === 'GlobalConstructor'); ok(globalConstructor.type.type[0].isFunction); expect(globalConstructor.type.kind).to.equal('Function'); expect(globalConstructor.type.type[0].isConstructor).to.equal(true); // Instance scope (should not be found) ok(!inRootScriptScope.find((id) => id.name === 'instance_function')); // Deeper local scope (should not be found) ok(!inRootScriptScope.find((id) => id.name === '_name')); //#endregion ROOT SCRIPT SCOPE //#region FUNCTION SCOPE // Params const paramName = '_name'; const paramRef = scriptFile.getReferenceAt({ line: 18, column: 32 }); ok(paramRef); expect(paramRef.text).to.equal(paramName); const param = paramRef.item; ok(param); ok(param.local); ok(param.parameter); expect(param.name).to.equal(paramName); // Params should be visible in the function scope const inFunctionScope = scriptFile.getInScopeSymbolsAt(19, 16); ok(inFunctionScope.length); ok(inFunctionScope.find((id) => id.name === paramName)); // And so should local vars const inFunctionLocalvar = inFunctionScope.find((id) => id.name === 'local'); ok(inFunctionLocalvar); ok(scriptFile.getReferenceAt(21, 10).item === inFunctionLocalvar); //#endregion FUNCTION SCOPE //#region INSTANCE SCOPE const instanceVarName = 'instance_variable'; const inInstanceScope = objStep.getReferenceAt(4, 9); const objectType = obj.instanceType; ok(objectType); ok(inInstanceScope); expect(inInstanceScope.text).to.equal(instanceVarName); ok(inInstanceScope.item.name === instanceVarName); ok(inInstanceScope.item.instance); // Are functions properly added to self? const instanceFunctionName = 'instance_function'; const instanceFunction = objectType.getMember(instanceFunctionName); ok(instanceFunction); //#endregion INSTANCE SCOPE //#region ENUMS const enumName = 'SurpriseEnum'; const enumMemberName = 'another_surprise'; const enumDef = scriptFile.getReferenceAt(22, 12); const enumMemberDef = scriptFile.getReferenceAt(22, 40); ok(enumDef); expect(enumDef.text).to.equal(enumName); ok(enumDef.item.name === enumName); ok(enumMemberDef); ok(enumMemberDef.item.name === enumMemberName); const enumRef = objCreate.getReferenceAt(3, 23); const enumMemberRef = objCreate.getReferenceAt(3, 38); ok(enumRef); ok(enumRef.item.name === enumName); ok(enumMemberRef); expect(enumMemberRef.text).to.equal(enumMemberName); ok(enumMemberRef.item.name === enumMemberName); //#endregion ENUMS //#region RECOVERY const enumAutocompleteScope = recoveryScriptFile.getScopeRangeAt(3, 22); ok(enumAutocompleteScope); const enumAutocompleteList = recoveryScriptFile.getInScopeSymbolsAt(3, 22); ok(enumAutocompleteList); expect(enumAutocompleteList.length).to.equal(2); //#endregion RECOVERY //#region CONSTRUCTORS const constructorName = 'GlobalConstructor'; const constructorDef = scriptFile.getReferenceAt(18, 17); const constructorSymbol = constructorDef.item; const constructorType = constructorSymbol.type; ok(constructorDef); expect(constructorDef.text).to.equal(constructorName); ok(constructorSymbol); ok(constructorType); ok(constructorSymbol.name === constructorName); ok(constructorSymbol instanceof Signifier); expect(constructorSymbol.type.kind).to.equal('Function'); expect(constructorSymbol.type.type[0].isConstructor).to.equal(true); expect(constructorType.type[0].name).to.equal(constructorName); expect(constructorType.type[0].listParameters()).to.have.lengthOf(2); expect(constructorType.constructs[0].kind).to.equal('Struct'); expect(constructorType.constructs[0].name).to.equal(constructorName); ok(project.self.getMember(constructorName) === constructorSymbol); ok(project.types.get(`Struct.${constructorName}`) === constructorType.constructs[0]); //#endregion CONSTRUCTORS //#region FUNCTION CALLS ok(!scriptFile.getFunctionArgRangeAt(29, 35).hasExpression); ok(scriptFile.getFunctionArgRangeAt(41, 50).hasExpression); //#endregion FUNCTION CALLS //#region DOT ASSIGNMENTS const dotAssignedRefName = 'another_instance_variable'; const dotAssignedRef = objCreate.getReferenceAt(20, 14); const dotAssignedType = dotAssignedRef?.item; ok(dotAssignedRef); expect(dotAssignedRef.text).to.equal(dotAssignedRefName); ok(dotAssignedRef.item.name === dotAssignedRefName); ok(dotAssignedType); ok(dotAssignedType.parent === obj.instanceType?.extends); //#endregion DOT ASSIGNMENTS //#region FUNCTIONS // Check the return type of a function const functionDefRef = complexScriptFile.getReferenceAt(119, 22); expect((functionDefRef?.item).type.returns[0].kind).to.equal('Array'); const globalFunction = scriptFile.getReferenceAt(6, 19); ok(globalFunction); ok(globalFunction.item.name === 'global_function'); //#endregion FUNCTIONS await validateCrossFileDiagnostics(project); validateGenerics(project); validateWithContexts(project); validateFunctionContexts(project); validateJsdocs(project); validateBschemaConstructor(project); await validateAccessorTypes(project); // Reprocess a file and ensure that the tests still pass await complexScriptFile.reload(); logger.log('Re-running after reload...'); validateBschemaConstructor(project); }); it('can sync datafiles', async function () { const project = await resetSandbox(); await project.dir .join('datafiles/test-folder/test-file.txt') .write('hello'); await project.syncIncludedFiles(); assert(project.datafiles.find((f) => f.name === 'test-file.txt' && f.filePath === 'datafiles/test-folder')); }); xit('can parse sample project', async function () { const projectDir = process.env.GML_PARSER_SAMPLE_PROJECT_DIR; ok(projectDir, 'A dotenv file should provide a path to a full sample project, as env var GML_PARSER_SAMPLE_PROJECT_DIR'); const project = await Project.initialize(projectDir); }); }); function validateGenerics(project) { const scriptFile = project.getAssetByName('Generics').gmlFile; const o_object = project.getAssetByName('o_object'); // SIMPLE IDENTIFY FUNCTION const identifyFunc = scriptFile.getReferenceAt(4, 15).item; const returns = identifyFunc.type.returns; expect(returns).to.have.lengthOf(1); expect(returns[0].type[0].name).to.equal('T'); const sampleType = scriptFile.getReferenceAt(8, 9).item.type.type[0]; const returnedSample = scriptFile.getReferenceAt(11, 11).item; const returnedSampleType = returnedSample.type.type[0]; ok(sampleType.kind === returnedSampleType.kind); // InstanceType<> const returnedInstance = scriptFile.getReferenceAt(24, 7).item.type.type[0]; ok(returnedInstance.kind === o_object.instanceType.kind); // ObjectType<> const returnedObject = scriptFile.getReferenceAt(25, 7).item.type.type[0]; ok(returnedObject.kind === o_object.assetType.kind); } async function validateCrossFileDiagnostics(project) { // In VSCode, we were finding that when a Create event was updated // then a struct stored in its variables would have incorrect "missing" // properties in the Step of the same object. const asset = project.getAssetByName('o_child1_child'); const createEvent = asset.getEventByName('Create_0'); const stepEvent = asset.getEventByName('Step_0'); const propName = 'idle'; const declaration = createEvent.getReferenceAt(5, 4); const reference = stepEvent.getReferenceAt(1, 20); const propertyDeclaration = createEvent.getReferenceAt(6, 5); const propertyReference = stepEvent.getReferenceAt(1, 25); assert(declaration.item === reference.item); assert(propertyDeclaration.item === propertyReference.item); assert(propertyDeclaration.item.name === propName); assert(declaration.item.type.type[0].getMember(propName) === propertyDeclaration.item); // Should not have any diagnostics in these files. const stepDiagnostics = stepEvent.getDiagnostics().UNDECLARED_VARIABLE_REFERENCE; expect(stepDiagnostics).to.have.lengthOf(0); // Upon reloading the Create event, should STILL have // no diagnostics and the references // should all go to the same, *defined* signifiers. await createEvent.reload(createEvent.content, { reloadDirty: true }); const reloadedDeclaration = createEvent.getReferenceAt(5, 4); assert(reloadedDeclaration.item === declaration.item); const reloadedReference = stepEvent.getReferenceAt(1, 20); expect(reloadedReference.item).to.equal(reloadedDeclaration.item); const reloadedPropertyDeclaration = createEvent.getReferenceAt(6, 5); assert(reloadedPropertyDeclaration.item.name === propName); assert(reloadedPropertyDeclaration.item === propertyDeclaration.item); const reloadedPropertyReference = stepEvent.getReferenceAt(1, 25); assert(reloadedPropertyReference.item === reloadedPropertyDeclaration.item); const reloadedStepDiagnostics = stepEvent.getDiagnostics().UNDECLARED_VARIABLE_REFERENCE; expect(reloadedStepDiagnostics).to.have.lengthOf(0); } async function validateAccessorTypes(project) { // There was an issue where the type retrieved from an accessor // would end up being a *new* type, causing structs to lose // properties upon reload // The sample case is in the `Reactions` script, where the // `_timer` localvar initially has the expected struct type // but after reloading it is missing most of its properties const reactionTimerFunction = project.self.getMember('ReactionTimer'); ok(reactionTimerFunction); const reactionTimerConstruct = reactionTimerFunction.type.constructs[0]; ok(reactionTimerConstruct); ok(reactionTimerConstruct.name === 'ReactionTimer'); const expectedMemberNames = [ 'reaction_id', 'timer', 'maxtime_min', 'maxtime_max', 'maxtime', 'event_id', ]; const assertAllMembersExist = (type) => { expectedMemberNames.forEach((name) => { ok(type.type[0].getMember(name), `Missing member ${name}`); }); }; const reactionsAsset = project.getAssetByName('Reactions'); const reactionsFile = reactionsAsset.gmlFile; const functionScope = reactionsFile.getScopeRangeAt(3, 1); ok(functionScope); const timerVar = functionScope.local.getMember('_timer'); ok(timerVar); ok(timerVar.type.type[0] === reactionTimerConstruct, 'Timer var type is not the expected type'); let withContext = reactionsFile.getScopeRangeAt(5, 1); ok(withContext.self === reactionTimerConstruct); assertAllMembersExist(timerVar.type); // Reload the file and ensure that the type is still correct await reactionsFile.reload(); const reloadedTimerVar = reactionsFile .getScopeRangeAt(3, 1) .local.getMember('_timer'); ok(reloadedTimerVar); ok(reloadedTimerVar.type.type[0] === reactionTimerConstruct, 'Timer var type is not the expected type after reload'); withContext = reactionsFile.getScopeRangeAt(5, 1); ok(withContext.self === reactionTimerConstruct); assertAllMembersExist(reloadedTimerVar.type); } function validateFunctionContexts(project) { const complicatedScriptFile = project.getAssetByName('Complicated').gmlFile; const functionScript = project.getAssetByName('FunctionSelf'); const functionScriptFile = functionScript.gmlFile; ok(functionScript); ok(functionScriptFile); ok(complicatedScriptFile); // GLOBAL CONSTRUCTED CONTEXT const bschemaGlobalContext = complicatedScriptFile.getReferenceAt(7, 14).item; const functionWithBschemaGlobalContext = functionScriptFile.getScopeRangeAt(2, 32); ok(functionWithBschemaGlobalContext && functionWithBschemaGlobalContext.self === bschemaGlobalContext.type.constructs[0]); // OBJECT CONTEXT const obj = project.getAssetByName('o_object'); ok(obj && obj.instanceType); const functionWithObjectContext = functionScriptFile.getScopeRangeAt(7, 25); ok(functionWithObjectContext && functionWithObjectContext.self === obj.assetType); // INSTANCE CONTEXT const functionWithInstanceContext = functionScriptFile.getScopeRangeAt(12, 27); ok(functionWithInstanceContext && functionWithInstanceContext.self === obj.instanceType); } function validateJsdocs(project) { const jsdocsFile = project.getAssetByName('Jsdocs').gmlFile; const jsdocs = jsdocsFile.jsdocs; expect(jsdocs).to.have.lengthOf(8); // Check positions let jsdoc = jsdocs[0]; expect(jsdoc.start.line).to.equal(1); expect(jsdoc.start.column).to.equal(1); expect(jsdoc.end.line).to.equal(11); expect(jsdoc.end.column).to.equal(16); expect(jsdoc.params[2].optional).to.equal(true); expect(jsdoc.params[2].name.content).to.equal('maybe'); expect(jsdoc.params[2].name.start.line).to.equal(6); expect(jsdoc.params[2].name.end.line).to.equal(6); expect(jsdoc.params[2].name.start.column).to.equal(22); expect(jsdoc.params[2].name.end.column).to.equal(26); expect(jsdoc.params[2].type.content).to.equal('Struct'); expect(jsdoc.params[2].type.start.line).to.equal(6); expect(jsdoc.params[2].type.end.line).to.equal(6); expect(jsdoc.params[2].type.start.column).to.equal(13); expect(jsdoc.params[2].type.end.column).to.equal(18); jsdoc = jsdocs[1]; expect(jsdoc.start.line).to.equal(13); expect(jsdoc.start.column).to.equal(3); expect(jsdoc.end.line).to.equal(14); expect(jsdoc.end.column).to.equal(21); jsdoc = jsdocs[2]; expect(jsdoc.start.line).to.equal(17); expect(jsdoc.start.column).to.equal(3); expect(jsdoc.end.line).to.equal(19); expect(jsdoc.end.column).to.equal(6); jsdoc = jsdocs[3]; expect(jsdoc.start.line).to.equal(23); expect(jsdoc.start.column).to.equal(1); expect(jsdoc.end.line).to.equal(34); expect(jsdoc.end.column).to.equal(38); // This one was misbehaving despite the above tests passing const scriptFile = project.getAssetByName('Script1').gmlFile; const lastJsdoc = scriptFile.jsdocs.at(-3); expect(lastJsdoc.start.line).to.equal(55); expect(lastJsdoc.start.column).to.equal(1); expect(lastJsdoc.end.line).to.equal(55); expect(lastJsdoc.end.column).to.equal(42); expect(lastJsdoc.params[0].name.content).to.equal('hello'); expect(lastJsdoc.params[0].type.start.line).to.equal(55); expect(lastJsdoc.params[0].type.end.line).to.equal(55); expect(lastJsdoc.params[0].type.start.column).to.equal(11); expect(lastJsdoc.params[0].type.end.column).to.equal(34); // Make sure that a JSDoc description without a type annotation // doesn't cause the type to be forced to `Any` const undescribedVar = jsdocsFile.getReferenceAt(42, 11).item; ok(undescribedVar); expect(undescribedVar.type.kind).to.equal('String'); const describedVar = jsdocsFile.getReferenceAt(44, 11).item; ok(describedVar); expect(describedVar.type.kind).to.equal('String'); } function validateWithContexts(project) { const complicatedScriptFile = project.getAssetByName('Complicated').gmlFile; const withingScript = project.getAssetByName('Withing'); const withingScriptFile = withingScript.gmlFile; ok(withingScript); ok(withingScriptFile); ok(complicatedScriptFile); // WITHING INTO A GLOBAL CONSTRUCTED const bschemaGlobalContext = complicatedScriptFile.getReferenceAt(1, 14).item .type.type[0]; ok(bschemaGlobalContext && bschemaGlobalContext.kind === 'Struct' && bschemaGlobalContext.name === 'Bschema'); const withIntoBschemaGlobal = withingScriptFile.getScopeRangeAt(2, 15); ok(withIntoBschemaGlobal && withIntoBschemaGlobal.self === bschemaGlobalContext); // WITHING INTO AN OBJECT IDENTIFIER const obj = project.getAssetByName('o_object'); ok(obj && obj.instanceType); const withIntoObject = withingScriptFile.getScopeRangeAt(6, 16); ok(withIntoObject && withIntoObject.self === obj.assetType); // TYPE TEXT const typeTextRef = withingScriptFile.getReferenceAt(10, 20); ok(typeTextRef); expect(typeTextRef.text).to.equal('Id.Instance.o_object'); expect(typeTextRef.isRenameable).to.equal(true); expect(typeTextRef.toRenamed('new_name')).to.equal('Id.Instance.new_name'); // WITHING INTO AN OBJECT INSTANCE const instanceVarRef = withingScriptFile.getReferenceAt(11, 11); const instanceVar = instanceVarRef.item; ok(instanceVar && instanceVar.type.type[0] === obj.instanceType); expect(instanceVarRef.text).to.equal('o_instance'); expect(instanceVarRef.isRenameable).to.equal(true); const withIntoInstance = withingScriptFile.getScopeRangeAt(13, 18); ok(withIntoInstance && withIntoInstance.self === obj.instanceType); // WITHING USING A JSDOC CONTEXT const withIntoJsdoc = withingScriptFile.getScopeRangeAt(20, 25); ok(withIntoJsdoc && withIntoJsdoc.self === obj.instanceType); // WITHING INTO LOCAL STRUCT const localStruct = withingScriptFile.getReferenceAt(24, 9).item; ok(localStruct && localStruct.type.kind === 'Struct'); const withIntoLocalStruct = withingScriptFile.getScopeRangeAt(27, 16); ok(withIntoLocalStruct && withIntoLocalStruct.self === localStruct.type.type[0]); // The following JSDoc was misbehaving at one point... const jsdoc = withingScriptFile.jsdocs[0]; ok(jsdoc); expect(jsdoc.type?.content).to.equal('Id.Instance.o_object'); expect(jsdoc.type?.start.column).to.equal(12); } function validateBschemaConstructor(project) { const complexScript = project.getAssetByName('Complicated'); const complexScriptFile = complexScript.gmlFile; const bschemaGlobal = project.self.getMember('BSCHEMA'); const bschemaStructType = project.types.get('Struct.Bschema'); const bschemaGlobalDef = complexScriptFile.getReferenceAt(1, 15); const bschemaConstructor = complexScriptFile.getReferenceAt(7, 13) ?.item; const bschemaRoleType = project.types.get('Struct.BschemaRole'); ok(bschemaGlobal); ok(bschemaStructType); ok(bschemaStructType.kind === 'Struct'); ok(bschemaGlobalDef); ok(bschemaGlobalDef.item === bschemaGlobal); ok(bschemaConstructor); ok(bschemaConstructor.type.kind === 'Function'); expect(bschemaConstructor.type.type[0].name).to.equal('Bschema'); ok(bschemaConstructor.type.constructs[0] === bschemaStructType); ok(bschemaRoleType); // Check all of the members of Struct.Bschema. // Make sure that the project_setup Bschema field gets typed based on its assignment const projectSetupAssignedTo = bschemaConstructor.type.type[0].getParameter(0); ok(projectSetupAssignedTo.name === 'project_setup_function'); // ok(projectSetupType === projectSetupAssignedTo.type); // Check the types of all of the fields of the Bschema struct const expectedKinds = { base: { kind: 'Struct' }, changes: { kind: 'Any' }, clear_changes: { kind: 'Function' }, commit_id_prefix: { kind: 'String' }, commitId: { kind: 'String' }, create_commit_id: { kind: 'Function' }, create_role: { kind: 'Function' }, force_use_packed: { kind: 'Bool' }, init: { kind: 'Function' }, latest_commitId: { kinds: ['String', 'Undefined'], code: 'String|Undefined', }, latest: { kind: 'String' }, next_commit_id: { kind: 'Function' }, packed_commitId: { kinds: ['String', 'Undefined'], code: 'String|Undefined', }, project_setup: { kind: 'Function' }, roles: { kind: 'Struct', code: 'Struct<Struct.BschemaRole>' }, schema_mote_ids: { kind: 'Struct', code: 'Struct<Array<String>>' }, uid_pools: { kind: 'Struct' }, }; for (const [fieldName, info] of Object.entries(expectedKinds)) { logger.log('Checking field', fieldName, 'of Bschema'); const member = bschemaStructType.getMember(fieldName); const type = member?.type; ok(member, `Expected to find member ${fieldName}`); ok(type, `Expected to find type for member ${fieldName}`); expect(member.name).to.equal(fieldName); if ('kind' in info) { expect(type.kind).to.equal(info.kind); } expect(member.def, 'All members should have a definition location').to .exist; if ('kinds' in info) { expect(type?.type.length).to.equal(info.kinds.length); for (const expectedKind of info.kinds) { expect(type?.type.some((t) => t.kind === expectedKind), `Type ${expectedKind} not found in Union`).to.be.true; } } if ('code' in info) { expect(type.toFeatherString()).to.equal(info.code); } } // Deeper checks on the types of some of the fields //#region Bschema.schema_mote_ids const schemaMoteIds = bschemaStructType.getMember('schema_mote_ids'); expect(schemaMoteIds.type.kind).to.equal('Struct'); expect(schemaMoteIds.type.items).to.exist; expect(schemaMoteIds.type.items[0].kind).to.equal('Array'); expect(schemaMoteIds.type.items[0].type[0].items).to.exist; expect(schemaMoteIds.type.items[0].type[0].items.kind).to.equal('String'); expect(schemaMoteIds.type.toFeatherString()).to.equal('Struct<Array<String>>'); //#endregion Bschema.schema_mote_ids //#region Bschema.roles const roles = bschemaStructType.getMember('roles'); expect(roles.type.kind).to.equal('Struct'); expect(roles.type.items[0]).to.exist; expect(roles.type.items[0].kind).to.equal('Struct'); ok(roles.type.items[0].type[0] === bschemaRoleType); //#endregion Bschema.roles } //# sourceMappingURL=project.test.js.map