diff --git a/src/index.ts b/src/index.ts index dadfdf5..8d6955b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -286,9 +286,10 @@ export class JSDocParser { const attribute = attributes[scriptName].attributes[name]; const isAttribute = !!attribute; + // null for names that aren't identifiers (#private, ['computed'], 'quoted', 42) const type = getType(member, typeChecker); - if (!SUPPORTED_ATTRIBUTE_TYPES.has(type.name)) { + if (!type || !SUPPORTED_ATTRIBUTE_TYPES.has(type.name)) { continue; } diff --git a/src/parsers/attribute-parser.ts b/src/parsers/attribute-parser.ts index 6eb2bd6..2848e13 100644 --- a/src/parsers/attribute-parser.ts +++ b/src/parsers/attribute-parser.ts @@ -191,7 +191,12 @@ export class AttributeParser { getNodeAsAttribute(node: ts.Node, errors: ParsingError[] = [], scriptName?: string) { const nameNode = (node as InternalNode).name; const name = (nameNode && ts.isIdentifier(nameNode) && nameNode.text) as string; - const { type, name: typeName, array, isMixedUnion } = getType(node, this.typeChecker); + + // null for names that aren't identifiers (#private, ['computed'], 'quoted', 42), which can't be attributes + const nodeType = getType(node, this.typeChecker); + if (!nodeType) return; + + const { type, name: typeName, array, isMixedUnion } = nodeType; const enums = this.getEnumMembers(node, errors); let value: LiteralValue = null; diff --git a/test/fixtures/member-names.valid.js b/test/fixtures/member-names.valid.js new file mode 100644 index 0000000..2bbacf8 --- /dev/null +++ b/test/fixtures/member-names.valid.js @@ -0,0 +1,47 @@ +/* eslint-disable */ +import { Script } from 'playcanvas'; + +/** + * @interface + */ +class Settings { + /** + * @attribute + * @type {number} + */ + level = 1; + + #cache = null; +} + +// Members whose names aren't identifiers (private, computed, quoted, numeric) can't be +// attributes. They must be skipped, not crash parsing of the whole script. +export class Example extends Script { + static scriptName = 'example'; + + /** + * @attribute + * @type {number} + */ + speed = 1; + + /** + * @attribute + * @type {Settings} + */ + settings; + + #secret = 2; + + /** + * @attribute + * @type {number} + */ + #taggedSecret = 3; + + ['computed'] = 4; + + 'quoted' = 5; + + 42 = 6; +} diff --git a/test/tests/valid/member-names.test.js b/test/tests/valid/member-names.test.js new file mode 100644 index 0000000..4ecd816 --- /dev/null +++ b/test/tests/valid/member-names.test.js @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { describe, it, before } from 'mocha'; + +import { getAttributes, parseAttributes } from '../../utils.ts'; + +describe('VALID: Members whose names are not identifiers', function () { + describe('parseAttributes', function () { + let data; + before(async function () { + data = await parseAttributes('./member-names.valid.js'); + }); + + it('parses without errors', function () { + expect(data[1]).to.be.empty; + expect(data[0]?.example).to.exist; + expect(data[0].example.errors).to.be.empty; + }); + + it('keeps the attributes with identifier names', function () { + expect(Object.keys(data[0].example.attributes)).to.have.members(['speed', 'settings']); + expect(data[0].example.attributes.speed.type).to.equal('number'); + }); + + it('skips a private member of an interface type', function () { + const schema = data[0].example.attributes.settings.schema; + expect(schema.map(({ name }) => name)).to.deep.equal(['level']); + }); + }); + + describe('getAttributes', function () { + let data; + before(async function () { + data = await getAttributes('./member-names.valid.js'); + }); + + it('parses without errors', function () { + expect(data[1]).to.be.empty; + expect(data[0]?.example).to.exist; + }); + + it('lists only the members with identifier names', function () { + const names = data[0].example.map(({ name }) => name); + expect(names).to.not.include.members(['#secret', '#taggedSecret', "['computed']", "'quoted'", '42']); + }); + }); +});