Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions src/parsers/script-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,18 +495,16 @@ export class ScriptParser {

// extract attributes for the Types properties
const symbol = type.aliasSymbol || type.symbol;
const typeNode = (symbol?.valueDeclaration || symbol.declarations?.[0]) as InternalNode;

if (!typeNode) {
// The attribute does not have a valid type
continue;
}
const typeNode = (symbol?.valueDeclaration || symbol?.declarations?.[0]) as InternalNode;

// Type constraints: Must be either an interface, typedef or initialized object literal, or a jsdoc type literal
const typeIsJSDocTypeDef = ts.isJSDocTypedefTag(typeNode);
const typeIsInterface = hasTag('interface', typeNode) || ts.isInterfaceDeclaration(typeNode);
const isInitialized = !!initializer && ts.isObjectLiteralExpression(initializer);
const isJSDocTypeLiteral = type.symbol.name === '__type';
// types without a declaration (tuples, unresolved or empty types) are reported as invalid below
const typeIsJSDocTypeDef = !!typeNode && ts.isJSDocTypedefTag(typeNode);
const typeIsInterface =
!!typeNode && (hasTag('interface', typeNode) || ts.isInterfaceDeclaration(typeNode));
const isInitialized =
!!initializer && ts.isObjectLiteralExpression(initializer) && initializer.properties.length > 0;
const isJSDocTypeLiteral = !!typeNode && type.symbol?.name === '__type';

// Check if the type is a valid interface
if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) {
Expand Down
62 changes: 62 additions & 0 deletions test/fixtures/type.invalid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Script } from 'playcanvas';

/** @interface */
class Enemy {
/** some data */
data = {};

/** @type {number} */
hp = 1;
}

class Example extends Script {
static scriptName = 'example';

/**
* @attribute
* @type {[number]}
*/
a;

/**
* @attribute
* @type {[number, number]}
*/
b;

/**
* @attribute
*/
c = {};

/**
* @attribute
* @type {Missing}
*/
d;

/**
* @attribute
* @type {Missing[]}
*/
e;

/**
* @attribute
* @type {{}}
*/
f;

/**
* @attribute
* @type {Enemy}
*/
g;

/**
* @attribute
*/
h = 1;
}

export { Example };
52 changes: 52 additions & 0 deletions test/tests/invalid/type.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect } from 'chai';
import { describe, it, before } from 'mocha';

import { parseAttributes } from '../../utils.ts';

describe('INVALID: Unsupported attribute types', function () {
let data;
before(async function () {
data = await parseAttributes('./type.invalid.js');
});

const errorFor = (name) => data[0].example.errors.find((e) => e.attributeName === name);

it('only results should exist', function () {
expect(data).to.exist;
expect(data[0]).to.not.be.empty;
expect(data[1]).to.be.empty;
});

it('Example: should exist with attributes and errors (7)', function () {
expect(data[0].example).to.exist;
expect(data[0].example.attributes).to.not.be.empty;
expect(data[0].example.errors.length).to.equal(7);
});

for (const [name, desc] of [
['a', 'a single element tuple'],
['b', 'a tuple'],
['c', 'an empty object initializer'],
['d', 'an unresolved type'],
['e', 'an unresolved array type'],
['f', 'an empty type literal']
]) {
it(`${name}: should report ${desc} as an invalid type`, function () {
expect(data[0].example.attributes[name]).to.not.exist;
expect(errorFor(name)?.message).to.include('is not a valid attribute type');
});
}

it('g: should report an empty object interface member and keep the rest', function () {
expect(errorFor('data')?.message).to.include('is not a valid attribute type');
expect(data[0].example.attributes.g).to.exist;
expect(data[0].example.attributes.g.type).to.equal('json');
expect(data[0].example.attributes.g.schema.map((s) => s.name)).to.deep.equal(['hp']);
});

it('h: should still parse valid attributes', function () {
expect(data[0].example.attributes.h).to.exist;
expect(data[0].example.attributes.h.type).to.equal('number');
expect(data[0].example.attributes.h.default).to.equal(1);
});
});
Loading