diff --git a/src/parsers/script-parser.ts b/src/parsers/script-parser.ts index 7549016..39141bf 100644 --- a/src/parsers/script-parser.ts +++ b/src/parsers/script-parser.ts @@ -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) { diff --git a/test/fixtures/type.invalid.js b/test/fixtures/type.invalid.js new file mode 100644 index 0000000..302c442 --- /dev/null +++ b/test/fixtures/type.invalid.js @@ -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 }; diff --git a/test/tests/invalid/type.test.js b/test/tests/invalid/type.test.js new file mode 100644 index 0000000..cf9c2f6 --- /dev/null +++ b/test/tests/invalid/type.test.js @@ -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); + }); +});