@bscotch/gml-parser
Version:
A parser for GML (GameMaker Language) files for programmatic manipulation and analysis of GameMaker projects.
143 lines • 6.37 kB
JavaScript
import { pathy } from '@bscotch/pathy';
import { undent } from '@bscotch/utility';
import { expect } from 'chai';
import dotenv from 'dotenv';
import fs from 'fs/promises';
import { logger } from './logger.js';
import { GmlParser } from './parser.js';
import { Type } from './types.js';
import { ok } from './util.js';
dotenv.config();
function showErrors(parser, filepath) {
const errors = Array.isArray(parser) ? parser : parser.errors;
if (!errors.length)
return;
logger.error(errors.map((e) => ({
loc: `${filepath || ''}:${e.token.startLine}:${e.token.startColumn}`,
msg: e.message,
token: e.token.image,
resynced: e.resyncedTokens.map((t) => ({
image: t.image,
startLine: t.startLine,
startColumn: t.startColumn,
})),
})));
}
describe('Parser', function () {
it('can get types from typestrings', function () {
expect(Type.fromFeatherString('Array', new Map(), false)[0].kind).to.equal('Array');
const stringArray = Type.fromFeatherString('Array<string>', new Map(), false)[0];
expect(stringArray.kind).to.equal('Array');
expect(stringArray.items.kind).to.equal('String');
const dsMap = Type.fromFeatherString('Id.DsMap[String,Real]', new Map(), false)[0];
expect(dsMap.kind).to.equal('Id.DsMap');
const dsMapItems = dsMap.items.type;
expect(dsMapItems.length).to.equal(2);
expect(dsMapItems[0].kind).to.equal('String');
expect(dsMapItems[1].kind).to.equal('Real');
});
it('can parse cross-referencing types', function () {
const knownTypes = new Map();
const arrayOfStructs = Type.fromFeatherString('Array<Struct.Hello>', knownTypes, true)[0];
knownTypes.set('Struct.Hello', arrayOfStructs.items.type[0]);
const structType = Type.fromFeatherString('Struct.Hello', knownTypes, false)[0];
ok(knownTypes.get('Struct.Hello') === structType);
expect(arrayOfStructs.kind).to.equal('Array');
expect(arrayOfStructs.items.kind).to.equal('Struct');
ok(arrayOfStructs.items.type[0] === structType);
expect(arrayOfStructs.items.type[0].name).to.equal('Hello');
});
it('can parse complex typestrings', function () {
const complexType = Type.fromFeatherString('Array<string OR Array<Real>>|Struct.Hello OR Id.DsMap[String,Real]', new Map(), true);
const [arrayType, structType, dsMapType] = complexType;
expect(complexType.length).to.equal(3);
expect(arrayType.kind).to.equal('Array');
const arrayItems = arrayType.items.type;
expect(arrayItems.length).to.equal(2);
expect(arrayItems[0].kind).to.equal('String');
expect(arrayItems[1].kind).to.equal('Array');
expect(arrayItems[1].items.kind).to.equal('Real');
expect(structType.kind).to.equal('Struct');
expect(structType.name).to.equal('Hello');
expect(dsMapType.kind).to.equal('Id.DsMap');
const dsMapItems = dsMapType.items.type;
expect(dsMapItems.length).to.equal(2);
expect(dsMapItems[0].kind).to.equal('String');
expect(dsMapItems[1].kind).to.equal('Real');
});
it('can parse simple expressions', function () {
const parser = new GmlParser();
const { cst } = parser.parse('(1 + 2 * 3) + (hello / (world || undefined))');
expect(parser.errors.length).to.equal(0);
expect(cst).to.exist;
});
it('can get errors for invalid simple expressions', function () {
const parser = new GmlParser();
const { cst, errors } = parser.parse('(1 + 2 * 3) + hello / world || undefined +');
expect(parser.errors.length).to.equal(1);
expect(errors.length).to.equal(1);
});
it('can parse complex expressions', function () {
const parser = new GmlParser();
const { cst } = parser.parse('1 + 2 * 3 + hello / world[no+true] || undefined + (1 + 2 * 3 + hello / world || undefined ^^ functionCall(10+3,undefined,,))');
expect(parser.errors.length).to.equal(0);
expect(cst).to.exist;
// logger.log(
// GmlParser.jsonify(
// // @ts-expect-error
// cst!.children.statement[0].children.expressionStatement[0].children
// .expression[0],
// ),
// );
});
it('can parse GML style JSDocs', function () {
const parser = new GmlParser();
const { cst } = parser.parse(undent `
/// @description This is a description
/// @param {string} a
/// @param {number} b
/// @returns {string}
function myFunc(a, b) {}
`);
showErrors(parser);
expect(parser.errors.length).to.equal(0);
expect(cst).to.exist;
});
it('can parse sample files', async function () {
const parser = new GmlParser();
const samples = await fs.readdir('./samples');
for (const sample of samples) {
// logger.log('Parsing', sample);
if (!sample.endsWith('.gml')) {
continue;
}
const filePath = `./samples/${sample}`;
const code = await fs.readFile(filePath, 'utf-8');
const { cst } = parser.parse(code);
showErrors(parser, filePath);
expect(cst).to.exist;
expect(parser.errors).to.have.length(0);
// logger.log(cst);
}
});
xit('can parse sample project', async function () {
const projectDir = process.env.GML_PARSER_SAMPLE_PROJECT_DIR;
expect(projectDir, 'A dotenv file should provide a path to a full sample project, as env var GML_PARSER_SAMPLE_PROJECT_DIR').to.exist;
const dir = pathy(projectDir);
const files = await dir.listChildrenRecursively({
includeExtension: ['.gml'],
});
expect(files.length).to.be.greaterThan(0);
const parser = new GmlParser();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const code = await file.read();
const { cst } = parser.parse(code);
showErrors(parser, file.absolute);
expect(cst).to.exist;
expect(parser.errors).to.have.length(0);
// logger.log(cst);
}
});
});
//# sourceMappingURL=parser.test.js.map