From 8b74d69a29237705b6f39d317decf700973bd602 Mon Sep 17 00:00:00 2001 From: Mark Lundin Date: Wed, 11 Sep 2024 16:56:45 +0100 Subject: [PATCH 1/3] Add new attribute classes and enums --- src/parsers/attribute-parser.js | 19 +++-- src/parsers/script-parser.js | 7 +- src/utils/ts-utils.js | 31 ++++++- test/fixtures/asset.invalid.ts | 24 ++++++ test/fixtures/asset.valid.ts | 21 +++++ test/fixtures/enum.valid.js | 18 +---- test/fixtures/enum.valid.ts | 49 +++++++++++ test/fixtures/json.valid.ts | 72 +++++++++++++++++ test/tests/valid/asset.test.js | 93 +++++++++++---------- test/tests/valid/checkbox.test.js | 90 +++++++++++---------- test/tests/valid/enum.test.js | 117 ++++++++++++++------------- test/tests/valid/json.test.js | 130 +++++++++++++++++++++++++++++- 12 files changed, 499 insertions(+), 172 deletions(-) create mode 100644 test/fixtures/asset.invalid.ts create mode 100644 test/fixtures/asset.valid.ts create mode 100644 test/fixtures/enum.valid.ts create mode 100644 test/fixtures/json.valid.ts diff --git a/src/parsers/attribute-parser.js b/src/parsers/attribute-parser.js index 670aee0..63cc1b6 100644 --- a/src/parsers/attribute-parser.js +++ b/src/parsers/attribute-parser.js @@ -4,7 +4,7 @@ import * as ts from 'typescript'; import { ParsingError } from './parsing-error.js'; import { hasTag } from '../utils/attribute-utils.js'; import { parseTag, validateTag } from '../utils/tag-utils.js'; -import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType } from '../utils/ts-utils.js'; +import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType, getPrimitiveEnumType } from '../utils/ts-utils.js'; /** * A class to parse JSDoc comments and extract attribute metadata. @@ -162,12 +162,14 @@ export class AttributeParser { getNodeAsAttribute(node, errors = []) { const name = node.name && ts.isIdentifier(node.name) && node.name.text; - const { type, name: typeName, array } = getType(node, this.typeChecker); + let { type, name: typeName, array } = getType(node, this.typeChecker); const enums = this.getEnumMembers(node, errors); let value = null; + // if(enums.length > 0 ) typeName = + // we don't need to serialize the value for arrays - const serializer = !array && this.typeSerializerMap.get(type); + const serializer = !array && this.typeSerializerMap.get(typeName); if (serializer) { try { value = serializer(node.initializer ?? node); @@ -192,8 +194,8 @@ export class AttributeParser { let members = []; // Check if there's a type annotation directly on the variable declaration - if (ts.isVariableDeclaration(node) && node.type) { - typeNode = node.type; + if (/*ts.isVariableDeclaration(node) && */node.type) { + typeNode = node; } else { // Check for JSDoc annotations const jsDocs = ts.getJSDocTags(node); @@ -203,10 +205,13 @@ export class AttributeParser { } } - if (typeNode && ts.isTypeReferenceNode(typeNode.type)) { + // Also consider the elementType + const type = typeNode && (typeNode.type.elementType ?? typeNode.type); + + if (typeNode && ts.isTypeReferenceNode(type)) { // resolve the symbol of the type - let symbol = this.typeChecker.getSymbolAtLocation(typeNode.type.typeName); + let symbol = this.typeChecker.getSymbolAtLocation(type.typeName); // Resolve aliases, which are common with imports if (symbol && symbol.flags & ts.SymbolFlags.Alias) { diff --git a/src/parsers/script-parser.js b/src/parsers/script-parser.js index 6907b33..feacc40 100644 --- a/src/parsers/script-parser.js +++ b/src/parsers/script-parser.js @@ -147,7 +147,8 @@ const SUPPORTED_BLOCK_TAGS = new Map([ ['precision', 'number'], ['size', 'number'], ['step', 'number'], - ['title', 'string'] + ['title', 'string'], + ['default', 'any'] ]); /** @@ -188,7 +189,7 @@ const mapAttributesToOutput = (attribute) => { if (attribute.enum.length === 0) delete attribute.enum; // set the default value - if (attribute.value !== undefined) attribute.default = attribute.value; + if (attribute.value !== undefined) attribute.default = attribute.default ?? attribute.value; // Curve Attributes specifically should not expose a default value if it's an empty array if (attribute.type === 'curve' && Array.isArray(attribute.value) && attribute.value.length === 0) { @@ -244,7 +245,7 @@ export class ScriptParser { const typeName = this.typeChecker.typeToString(type); const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName); if (serializer) { - this.typeSerializerMap.set(type, serializer); + this.typeSerializerMap.set(typeName, serializer); } }); diff --git a/src/utils/ts-utils.js b/src/utils/ts-utils.js index 833ebd2..4dab618 100644 --- a/src/utils/ts-utils.js +++ b/src/utils/ts-utils.js @@ -208,7 +208,7 @@ function getSuperClasses(node, typeChecker) { export function getJSDocCommentRanges(node, text, typeChecker) { const commentRanges = []; - if (ts.isClassDeclaration(node)) { + if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) { // get an array of the class an all parent classed const heritageChain = getSuperClasses(node, typeChecker); @@ -216,7 +216,7 @@ export function getJSDocCommentRanges(node, text, typeChecker) { heritageChain.forEach((classNode) => { // for each class iterate over it's class members classNode.members.forEach((member) => { - if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member)) { + if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member) || ts.isPropertySignature(member)) { const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed'; const ranges = getLeadingBlockCommentRanges(member, text); if (ranges.length > 0) { @@ -309,6 +309,31 @@ export function isEnum(node) { return false; } +/** + * Determines the primitive type for enums, or falls back to the actual type name. + * + * @param {ts.Type} type - The type to inspect. + * @param {ts.TypeChecker} typeChecker - The TypeScript type checker. + * @returns {string} - The primitive type of the enum or the type's name. + */ +export function getPrimitiveEnumType(type, typeChecker) { + // Check if the type is an enum type + if (type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) { + // Get the type of enum members + const enumMembers = type.symbol.declarations[0].members; + const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]); + + const validEnumType = [ + 'number', + 'string', + 'boolean' + ] + + if (validEnumType.includes(typeof firstMemberValue)) { + return typeof firstMemberValue; + } + } +} /** * Gets the inferred type of a TypeScript node. @@ -322,7 +347,7 @@ export function getType(node, typeChecker) { const type = typeChecker.getTypeAtLocation(node); const array = typeChecker.isArrayType(type); const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type; - const name = typeChecker.typeToString(actualType); + const name = getPrimitiveEnumType(actualType, typeChecker) ?? typeChecker.typeToString(actualType); return { type: actualType, name, array }; } diff --git a/test/fixtures/asset.invalid.ts b/test/fixtures/asset.invalid.ts new file mode 100644 index 0000000..5fc7cbf --- /dev/null +++ b/test/fixtures/asset.invalid.ts @@ -0,0 +1,24 @@ +// eslint-disable-next-line +import { Script, Asset } from 'playcanvas'; + +class Example extends Script { + /** + * @attribute + * @resource nothing + */ + a: Asset; + + /** + * @attribute + * @resource 1 + */ + b : Asset; + + /** + * @attribute + * @resource + */ + c : Asset; +} + +export { Example }; diff --git a/test/fixtures/asset.valid.ts b/test/fixtures/asset.valid.ts new file mode 100644 index 0000000..800b321 --- /dev/null +++ b/test/fixtures/asset.valid.ts @@ -0,0 +1,21 @@ +// eslint-disable-next-line +import { Script, Asset } from 'playcanvas'; + +class Example extends Script { + /** @attribute */ + a : Asset; + + /** + * @attribute + * @resource texture + */ + b : Asset; + + /** + * @attribute + * @resource container + */ + c : Asset[]; +} + +export { Example }; diff --git a/test/fixtures/enum.valid.js b/test/fixtures/enum.valid.js index 049146d..695a661 100644 --- a/test/fixtures/enum.valid.js +++ b/test/fixtures/enum.valid.js @@ -19,16 +19,6 @@ const StringEnum = { C: 'c' }; -/** - * @enum {Vec3} - */ -// eslint-disable-next-line -const Vec3Enum = { - A: new Vec3(1, 2, 3), - B: new Vec3(4, 5, 6), - C: new Vec3(7, 8, 9) -}; - /** * @enum {NumberEnum} */ @@ -65,17 +55,11 @@ class Example extends Script { */ h; - /** - * @attribute - * @type {Vec3Enum} - */ - i; - /** * @attribute * @type {NumberNumberEnum} */ - j = 2; + i = 2; } export { Example }; diff --git a/test/fixtures/enum.valid.ts b/test/fixtures/enum.valid.ts new file mode 100644 index 0000000..a721a07 --- /dev/null +++ b/test/fixtures/enum.valid.ts @@ -0,0 +1,49 @@ +import { Script, Vec3 } from 'playcanvas'; + +enum NumberEnum { + A = 0, + B = 1, + C = 2 +}; + +enum StringEnum { + A = 'a', + B = 'b', + C = 'c' +}; + +enum NumberNumberEnum { + A = NumberEnum.A, + B = NumberEnum.B, + C = NumberEnum.C +}; + +class Example extends Script { + /** + * @attribute + */ + e : NumberEnum; + + /** + * @attribute + */ + f : NumberEnum = 1; + + /** + * @attribute + * @size 2 + */ + g : NumberEnum[]; + + /** + * @attribute + */ + h : StringEnum; + + /** + * @attribute + */ + i : NumberNumberEnum = 2; +} + +export { Example }; diff --git a/test/fixtures/json.valid.ts b/test/fixtures/json.valid.ts new file mode 100644 index 0000000..a28b66e --- /dev/null +++ b/test/fixtures/json.valid.ts @@ -0,0 +1,72 @@ +import { Script, Vec3 } from 'playcanvas'; + +interface Folder { + /** + * @attribute + */ + a: boolean; + + /** + * @attribute + * @default 10 + */ + b: number; + + /** + * @attribute + * @default hello + */ + c : string; + + /** + * @attribute + */ + d : Vec3[]; +} + +interface NestedFolder { + /** + * @attribute + */ + x : Folder; +} + +/** + * @interface + */ +class Example extends Script { + /** + * @attribute + */ + f : Folder; + + /** + * @attribute + * @size 2 + */ + g : Folder[]; + + /** + * @attribute + */ + h : NestedFolder; + + /** + * @attribute + */ + i = { a: true, b: 10, c: 'hello', d: [new Vec3(1, 2, 3)] }; + + /** + * @attribute + */ + l : { x: number, y: number }; + + /** + * @attribute + */ + m : Example; + + n = 10; +} + +export { Example }; diff --git a/test/tests/valid/asset.test.js b/test/tests/valid/asset.test.js index f43a23a..9d13c91 100644 --- a/test/tests/valid/asset.test.js +++ b/test/tests/valid/asset.test.js @@ -3,48 +3,57 @@ import { describe, it, before } from 'mocha'; import { parseAttributes } from '../../utils.js'; -describe('VALID: Asset attribute', function () { - let data; - before(async function () { - data = await parseAttributes('./asset.valid.js'); - }); - - 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 without errors', function () { - expect(data[0]?.example).to.exist; - expect(data[0].example.attributes).to.not.be.empty; - expect(data[0].example.errors).to.be.empty; - }); - - it('a: should be a asset attribute', function () { - expect(data[0].example.attributes.a).to.exist; - expect(data[0].example.attributes.a.name).to.equal('a'); - expect(data[0].example.attributes.a.type).to.equal('asset'); - expect(data[0].example.attributes.a.array).to.equal(false); - expect(data[0].example.attributes.a.default).to.equal(null); - }); - - it('b: should be a asset attribute with a texture resource', function () { - expect(data[0].example.attributes.b).to.exist; - expect(data[0].example.attributes.b.name).to.equal('b'); - expect(data[0].example.attributes.b.type).to.equal('asset'); - expect(data[0].example.attributes.b.array).to.equal(false); - expect(data[0].example.attributes.b.default).to.equal(null); - expect(data[0].example.attributes.b.resource).to.equal('texture'); - }); +function runTests(fileName) { + + const isTS= fileName.endsWith('.ts'); + const script = isTS ? 'TypeScript' : 'JavaScript'; + + describe(`${script}: VALID: Asset attribute`, function () { + let data; + before(async function () { + data = await parseAttributes(fileName); + }); + + 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 without errors', function () { + expect(data[0]?.example).to.exist; + expect(data[0].example.attributes).to.not.be.empty; + expect(data[0].example.errors).to.be.empty; + }); + + it('a: should be a asset attribute', function () { + expect(data[0].example.attributes.a).to.exist; + expect(data[0].example.attributes.a.name).to.equal('a'); + expect(data[0].example.attributes.a.type).to.equal('asset'); + expect(data[0].example.attributes.a.array).to.equal(false); + expect(data[0].example.attributes.a.default).to.equal(null); + }); + + it('b: should be a asset attribute with a texture resource', function () { + expect(data[0].example.attributes.b).to.exist; + expect(data[0].example.attributes.b.name).to.equal('b'); + expect(data[0].example.attributes.b.type).to.equal('asset'); + expect(data[0].example.attributes.b.array).to.equal(false); + expect(data[0].example.attributes.b.default).to.equal(null); + expect(data[0].example.attributes.b.resource).to.equal('texture'); + }); + + it('c: should be a asset attribute array with a container resource', function () { + expect(data[0].example.attributes.c).to.exist; + expect(data[0].example.attributes.c.name).to.equal('c'); + expect(data[0].example.attributes.c.type).to.equal('asset'); + expect(data[0].example.attributes.c.array).to.equal(true); + expect(data[0].example.attributes.c.default).to.equal(null); + expect(data[0].example.attributes.c.resource).to.equal('container'); + }); - it('c: should be a asset attribute array with a container resource', function () { - expect(data[0].example.attributes.c).to.exist; - expect(data[0].example.attributes.c.name).to.equal('c'); - expect(data[0].example.attributes.c.type).to.equal('asset'); - expect(data[0].example.attributes.c.array).to.equal(true); - expect(data[0].example.attributes.c.default).to.equal(null); - expect(data[0].example.attributes.c.resource).to.equal('container'); }); +}; -}); +runTests('./asset.valid.js'); +runTests('./asset.valid.ts'); diff --git a/test/tests/valid/checkbox.test.js b/test/tests/valid/checkbox.test.js index 6bac811..622d95f 100644 --- a/test/tests/valid/checkbox.test.js +++ b/test/tests/valid/checkbox.test.js @@ -3,47 +3,55 @@ import { describe, it, before } from 'mocha'; import { parseAttributes } from '../../utils.js'; -describe('VALID: Checkbox attribute', function () { - let data; - before(async function () { - data = await parseAttributes('./checkbox.valid.js'); - }); - - 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 without errors', function () { - expect(data[0]?.example).to.exist; - expect(data[0].example.attributes).to.not.be.empty; - expect(data[0].example.errors).to.be.empty; - }); - - it('a: should be a checkbox attribute', function () { - expect(data[0].example.attributes.a).to.exist; - expect(data[0].example.attributes.a.name).to.equal('a'); - expect(data[0].example.attributes.a.type).to.equal('boolean'); - expect(data[0].example.attributes.a.array).to.equal(false); - expect(data[0].example.attributes.a.default).to.equal(false); - }); - - it('b: should be a checkbox attribute with a default value', function () { - expect(data[0].example.attributes.b).to.exist; - expect(data[0].example.attributes.b.name).to.equal('b'); - expect(data[0].example.attributes.b.type).to.equal('boolean'); - expect(data[0].example.attributes.b.array).to.equal(false); - expect(data[0].example.attributes.b.default).to.equal(true); - }); +function runTests(fileName) { + + const isTS= fileName.endsWith('.ts'); + const script = isTS ? 'TypeScript' : 'JavaScript'; + + describe(`${script}: VALID: Checkbox attribute`, function () { + let data; + before(async function () { + data = await parseAttributes(fileName); + }); + + 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 without errors', function () { + expect(data[0]?.example).to.exist; + expect(data[0].example.attributes).to.not.be.empty; + expect(data[0].example.errors).to.be.empty; + }); + + it('a: should be a checkbox attribute', function () { + expect(data[0].example.attributes.a).to.exist; + expect(data[0].example.attributes.a.name).to.equal('a'); + expect(data[0].example.attributes.a.type).to.equal('boolean'); + expect(data[0].example.attributes.a.array).to.equal(false); + expect(data[0].example.attributes.a.default).to.equal(false); + }); + + it('b: should be a checkbox attribute with a default value', function () { + expect(data[0].example.attributes.b).to.exist; + expect(data[0].example.attributes.b.name).to.equal('b'); + expect(data[0].example.attributes.b.type).to.equal('boolean'); + expect(data[0].example.attributes.b.array).to.equal(false); + expect(data[0].example.attributes.b.default).to.equal(true); + }); + + it('c: should be a checkbox attribute array with a size', function () { + expect(data[0].example.attributes.c).to.exist; + expect(data[0].example.attributes.c.name).to.equal('c'); + expect(data[0].example.attributes.c.type).to.equal('boolean'); + expect(data[0].example.attributes.c.array).to.equal(true); + expect(data[0].example.attributes.c.size).to.equal(2); + expect(data[0].example.attributes.c.default).equal(null); + }); - it('c: should be a checkbox attribute array with a size', function () { - expect(data[0].example.attributes.c).to.exist; - expect(data[0].example.attributes.c.name).to.equal('c'); - expect(data[0].example.attributes.c.type).to.equal('boolean'); - expect(data[0].example.attributes.c.array).to.equal(true); - expect(data[0].example.attributes.c.size).to.equal(2); - expect(data[0].example.attributes.c.default).equal(null); }); +} -}); +runTests('./checkbox.valid.js'); diff --git a/test/tests/valid/enum.test.js b/test/tests/valid/enum.test.js index bcb113d..894de89 100644 --- a/test/tests/valid/enum.test.js +++ b/test/tests/valid/enum.test.js @@ -3,71 +3,72 @@ import { describe, it, before } from 'mocha'; import { parseAttributes } from '../../utils.js'; -describe('VALID: Enum attribute', function () { - let data; - before(async function () { - data = await parseAttributes('./enum.valid.js'); - }); +function runTests(fileName) { - it('only results should exist', function () { - expect(data).to.exist; - expect(data[0]).to.not.be.empty; - expect(data[1]).to.be.empty; - }); + const isTS= fileName.endsWith('.ts'); + const script = isTS ? 'TypeScript' : 'JavaScript'; - it('Example: should exist without errors', function () { - expect(data[0]?.example).to.exist; - expect(data[0].example.attributes).to.not.be.empty; - expect(data[0].example.errors).to.be.empty; - }); + describe(`${script}: VALID: Enum attribute`, function () { + let data; + before(async function () { + data = await parseAttributes(fileName); + }); - it('e: should be a enum attribute', function () { - expect(data[0].example.attributes.e).to.exist; - expect(data[0].example.attributes.e.name).to.equal('e'); - expect(data[0].example.attributes.e.type).to.equal('number'); - expect(data[0].example.attributes.e.array).to.equal(false); - expect(data[0].example.attributes.e.default).to.equal(0); - }); + it('only results should exist', function () { + expect(data).to.exist; + expect(data[0]).to.not.be.empty; + expect(data[1]).to.be.empty; + }); - it('f: should be a enum attribute with a default value', function () { - expect(data[0].example.attributes.f).to.exist; - expect(data[0].example.attributes.f.name).to.equal('f'); - expect(data[0].example.attributes.f.type).to.equal('number'); - expect(data[0].example.attributes.f.array).to.equal(false); - expect(data[0].example.attributes.f.default).to.equal(1); - }); + it('Example: should exist without errors', function () { + expect(data[0]?.example).to.exist; + expect(data[0].example.errors).to.be.empty; + expect(data[0].example.attributes).to.not.be.empty; + }); - it('g: should be a enum attribute array with a size', function () { - expect(data[0].example.attributes.g).to.exist; - expect(data[0].example.attributes.g.name).to.equal('g'); - expect(data[0].example.attributes.g.type).to.equal('number'); - expect(data[0].example.attributes.g.array).to.equal(true); - expect(data[0].example.attributes.g.size).to.equal(2); - expect(data[0].example.attributes.g.default).equal(null); - }); + it('e: should be a enum attribute', function () { + expect(data[0].example.attributes.e).to.exist; + expect(data[0].example.attributes.e.name).to.equal('e'); + expect(data[0].example.attributes.e.type).to.equal('number'); + expect(data[0].example.attributes.e.array).to.equal(false); + expect(data[0].example.attributes.e.default).to.equal(0); + }); - it('h: should be a enum attribute', function () { - expect(data[0].example.attributes.h).to.exist; - expect(data[0].example.attributes.h.name).to.equal('h'); - expect(data[0].example.attributes.h.type).to.equal('string'); - expect(data[0].example.attributes.h.array).to.equal(false); - expect(data[0].example.attributes.h.default).to.equal(''); - }); + it('f: should be a enum attribute with a default value', function () { + expect(data[0].example.attributes.f).to.exist; + expect(data[0].example.attributes.f.name).to.equal('f'); + expect(data[0].example.attributes.f.type).to.equal('number'); + expect(data[0].example.attributes.f.array).to.equal(false); + expect(data[0].example.attributes.f.default).to.equal(1); + }); - it('i: should be a enum attribute with a default value', function () { - expect(data[0].example.attributes.i).to.exist; - expect(data[0].example.attributes.i.name).to.equal('i'); - expect(data[0].example.attributes.i.type).to.equal('vec3'); - expect(data[0].example.attributes.i.array).to.equal(false); - expect(data[0].example.attributes.i.default).to.eql([0, 0, 0]); - }); + it('g: should be a enum attribute array with a size', function () { + expect(data[0].example.attributes.g).to.exist; + expect(data[0].example.attributes.g.name).to.equal('g'); + expect(data[0].example.attributes.g.type).to.equal('number'); + expect(data[0].example.attributes.g.array).to.equal(true); + expect(data[0].example.attributes.g.size).to.equal(2); + expect(data[0].example.attributes.g.default).equal(null); + }); + + it('h: should be a enum attribute', function () { + expect(data[0].example.attributes.h).to.exist; + expect(data[0].example.attributes.h.name).to.equal('h'); + expect(data[0].example.attributes.h.type).to.equal('string'); + expect(data[0].example.attributes.h.array).to.equal(false); + expect(data[0].example.attributes.h.default).to.equal(''); + }); + + it('j: should be a enum attribute array with a size', function () { + expect(data[0].example.attributes.i).to.exist; + expect(data[0].example.attributes.i.name).to.equal('i'); + expect(data[0].example.attributes.i.type).to.equal('number'); + expect(data[0].example.attributes.i.array).to.equal(false); + expect(data[0].example.attributes.i.default).equal(2); + }); - it('j: should be a enum attribute array with a size', function () { - expect(data[0].example.attributes.j).to.exist; - expect(data[0].example.attributes.j.name).to.equal('j'); - expect(data[0].example.attributes.j.type).to.equal('number'); - expect(data[0].example.attributes.j.array).to.equal(false); - expect(data[0].example.attributes.j.default).equal(2); }); +} -}); +runTests('./enum.valid.js'); +runTests('./enum.valid.ts'); diff --git a/test/tests/valid/json.test.js b/test/tests/valid/json.test.js index 4e2356a..79716f5 100644 --- a/test/tests/valid/json.test.js +++ b/test/tests/valid/json.test.js @@ -3,7 +3,8 @@ import { describe, it, before } from 'mocha'; import { parseAttributes } from '../../utils.js'; -describe('VALID: JSON attribute', function () { +describe(`Javascript: VALID: Asset attribute`, function () { + let data; before(async function () { data = await parseAttributes('./json.valid.js'); @@ -147,3 +148,130 @@ describe('VALID: JSON attribute', function () { expect(data[0].example.attributes.o).to.not.exist; }); }); + + +describe(`Typescript: VALID: Asset attribute`, function () { + + let data; + before(async function () { + data = await parseAttributes('./json.valid.ts'); + }); + + 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 without errors', function () { + expect(data[0]?.example).to.exist; + expect(data[0].example.attributes).to.not.be.empty; + expect(data[0].example.errors).to.be.empty; + }); + + it('f: should be a json attribute', function () { + expect(data[0].example.attributes.f).to.exist; + expect(data[0].example.attributes.f.name).to.equal('f'); + expect(data[0].example.attributes.f.type).to.equal('json'); + expect(data[0].example.attributes.f.array).to.equal(false); + expect(data[0].example.attributes.f.default).to.equal(null); + + expect(data[0].example.attributes.f.schema).to.exist; + }); + + it('f[a]: should be a boolean attribute', function () { + expect(data[0].example.attributes.f.schema[0].name).to.equal('a'); + expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean'); + expect(data[0].example.attributes.f.schema[0].array).to.equal(false); + expect(data[0].example.attributes.f.schema[0].default).to.equal(false); + }); + + it('f[b]: should be a numeric attribute', function () { + expect(data[0].example.attributes.f.schema[1].name).to.equal('b'); + expect(data[0].example.attributes.f.schema[1].type).to.equal('number'); + expect(data[0].example.attributes.f.schema[1].array).to.equal(false); + expect(data[0].example.attributes.f.schema[1].default).to.equal(10); + }); + + it('f[c]: should be a string attribute', function () { + expect(data[0].example.attributes.f.schema[2].name).to.equal('c'); + expect(data[0].example.attributes.f.schema[2].type).to.equal('string'); + expect(data[0].example.attributes.f.schema[2].array).to.equal(false); + expect(data[0].example.attributes.f.schema[2].default).to.equal('hello'); + }); + + it('f[d]: should be a vec3 attribute array', function () { + expect(data[0].example.attributes.f.schema[3].name).to.equal('d'); + expect(data[0].example.attributes.f.schema[3].type).to.equal('vec3'); + expect(data[0].example.attributes.f.schema[3].array).to.equal(true); + expect(data[0].example.attributes.f.schema[3].default).to.eql(null); + }); + + it('g: should be a json attribute array with a size', function () { + expect(data[0].example.attributes.g).to.exist; + expect(data[0].example.attributes.g.name).to.equal('g'); + expect(data[0].example.attributes.g.type).to.equal('json'); + expect(data[0].example.attributes.g.array).to.equal(true); + expect(data[0].example.attributes.g.size).to.equal(2); + expect(data[0].example.attributes.g.default).to.eql(null); + + expect(data[0].example.attributes.g.schema).to.exist; + }); + + it('h: should be a nested json attribute (parent)', function () { + expect(data[0].example.attributes.h).to.exist; + expect(data[0].example.attributes.h.name).to.equal('h'); + expect(data[0].example.attributes.h.type).to.equal('json'); + expect(data[0].example.attributes.h.array).to.equal(false); + expect(data[0].example.attributes.h.default).to.eql(null); + + expect(data[0].example.attributes.h.schema).to.exist; + }); + + it('h[x]: should be a nested json attribute (child)', function () { + expect(data[0].example.attributes.h.schema[0].name).to.equal('x'); + expect(data[0].example.attributes.h.schema[0].type).to.equal('json'); + expect(data[0].example.attributes.h.schema[0].array).to.equal(false); + expect(data[0].example.attributes.h.schema[0].default).to.eql(null); + + expect(data[0].example.attributes.h.schema[0].schema).to.exist; + }); + + it('i: should be an inline default json attribute', function () { + expect(data[0].example.attributes.i).to.exist; + expect(data[0].example.attributes.i.name).to.equal('i'); + expect(data[0].example.attributes.i.type).to.equal('json'); + expect(data[0].example.attributes.i.array).to.equal(false); + expect(data[0].example.attributes.i.default).to.eql(null); + + expect(data[0].example.attributes.i.schema).to.exist; + }); + + it('l: should be an inline type json attribute', function () { + expect(data[0].example.attributes.l).to.exist; + expect(data[0].example.attributes.l.name).to.equal('l'); + expect(data[0].example.attributes.l.type).to.equal('json'); + expect(data[0].example.attributes.l.array).to.equal(false); + expect(data[0].example.attributes.l.default).to.eql(null); + + expect(data[0].example.attributes.l.schema).to.exist; + }); + + it('m: should be a self reference json attribute', function () { + expect(data[0].example.attributes.m).to.exist; + expect(data[0].example.attributes.m.name).to.equal('m'); + expect(data[0].example.attributes.m.type).to.equal('json'); + expect(data[0].example.attributes.m.array).to.equal(false); + expect(data[0].example.attributes.m.default).to.eql(null); + + expect(data[0].example.attributes.m.schema).to.exist; + }); + + it('n: should not be recognised as an attribute (no attribute tag)', function () { + expect(data[0].example.attributes.n).to.not.exist; + }); + + it('o: should not be recognised as an attribute (type not an interface)', function () { + expect(data[0].example.attributes.o).to.not.exist; + }); +}); From ceb8f9f135a7c059ea9fb63636175f1190f8a6a2 Mon Sep 17 00:00:00 2001 From: Mark Lundin Date: Wed, 11 Sep 2024 17:26:32 +0100 Subject: [PATCH 2/3] Fix import in attribute-parser.js and remove unused code in ts-utils.js --- src/parsers/attribute-parser.js | 8 +++----- src/utils/ts-utils.js | 32 +++++++++++++++---------------- test/fixtures/enum.valid.js | 2 +- test/tests/valid/asset.test.js | 6 +++--- test/tests/valid/checkbox.test.js | 4 ++-- test/tests/valid/enum.test.js | 4 ++-- test/tests/valid/json.test.js | 4 ++-- 7 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/parsers/attribute-parser.js b/src/parsers/attribute-parser.js index 63cc1b6..85b4e0e 100644 --- a/src/parsers/attribute-parser.js +++ b/src/parsers/attribute-parser.js @@ -4,7 +4,7 @@ import * as ts from 'typescript'; import { ParsingError } from './parsing-error.js'; import { hasTag } from '../utils/attribute-utils.js'; import { parseTag, validateTag } from '../utils/tag-utils.js'; -import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType, getPrimitiveEnumType } from '../utils/ts-utils.js'; +import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType } from '../utils/ts-utils.js'; /** * A class to parse JSDoc comments and extract attribute metadata. @@ -162,12 +162,10 @@ export class AttributeParser { getNodeAsAttribute(node, errors = []) { const name = node.name && ts.isIdentifier(node.name) && node.name.text; - let { type, name: typeName, array } = getType(node, this.typeChecker); + const { type, name: typeName, array } = getType(node, this.typeChecker); const enums = this.getEnumMembers(node, errors); let value = null; - // if(enums.length > 0 ) typeName = - // we don't need to serialize the value for arrays const serializer = !array && this.typeSerializerMap.get(typeName); if (serializer) { @@ -194,7 +192,7 @@ export class AttributeParser { let members = []; // Check if there's a type annotation directly on the variable declaration - if (/*ts.isVariableDeclaration(node) && */node.type) { + if (node.type) { typeNode = node; } else { // Check for JSDoc annotations diff --git a/src/utils/ts-utils.js b/src/utils/ts-utils.js index 4dab618..4a54fef 100644 --- a/src/utils/ts-utils.js +++ b/src/utils/ts-utils.js @@ -309,30 +309,30 @@ export function isEnum(node) { return false; } + /** * Determines the primitive type for enums, or falls back to the actual type name. * * @param {ts.Type} type - The type to inspect. * @param {ts.TypeChecker} typeChecker - The TypeScript type checker. - * @returns {string} - The primitive type of the enum or the type's name. + * @returns {'string' | 'boolean' | 'number' | null} - The primitive type of the enum or the type's name. */ export function getPrimitiveEnumType(type, typeChecker) { // Check if the type is an enum type - if (type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) { - // Get the type of enum members - const enumMembers = type.symbol.declarations[0].members; - const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]); - - const validEnumType = [ - 'number', - 'string', - 'boolean' - ] - - if (validEnumType.includes(typeof firstMemberValue)) { - return typeof firstMemberValue; - } - } + if (!type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) return null; + + // Get the type of enum members + const enumMembers = type.symbol.declarations[0].members; + const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]); + + const validEnumType = [ + 'number', + 'string', + 'boolean' + ]; + + const typeOf = typeof firstMemberValue; + return validEnumType.includes(typeOf) ? typeOf : null; } /** diff --git a/test/fixtures/enum.valid.js b/test/fixtures/enum.valid.js index 695a661..3a14f0f 100644 --- a/test/fixtures/enum.valid.js +++ b/test/fixtures/enum.valid.js @@ -1,4 +1,4 @@ -import { Script, Vec3 } from 'playcanvas'; +import { Script } from 'playcanvas'; /** * @enum {number} diff --git a/test/tests/valid/asset.test.js b/test/tests/valid/asset.test.js index 9d13c91..190fbf9 100644 --- a/test/tests/valid/asset.test.js +++ b/test/tests/valid/asset.test.js @@ -5,8 +5,8 @@ import { parseAttributes } from '../../utils.js'; function runTests(fileName) { - const isTS= fileName.endsWith('.ts'); - const script = isTS ? 'TypeScript' : 'JavaScript'; + const isTS = fileName.endsWith('.ts'); + const script = isTS ? 'TS' : 'JS'; describe(`${script}: VALID: Asset attribute`, function () { let data; @@ -53,7 +53,7 @@ function runTests(fileName) { }); }); -}; +} runTests('./asset.valid.js'); runTests('./asset.valid.ts'); diff --git a/test/tests/valid/checkbox.test.js b/test/tests/valid/checkbox.test.js index 622d95f..fd8dba3 100644 --- a/test/tests/valid/checkbox.test.js +++ b/test/tests/valid/checkbox.test.js @@ -5,8 +5,8 @@ import { parseAttributes } from '../../utils.js'; function runTests(fileName) { - const isTS= fileName.endsWith('.ts'); - const script = isTS ? 'TypeScript' : 'JavaScript'; + const isTS = fileName.endsWith('.ts'); + const script = isTS ? 'TS' : 'JS'; describe(`${script}: VALID: Checkbox attribute`, function () { let data; diff --git a/test/tests/valid/enum.test.js b/test/tests/valid/enum.test.js index 894de89..6c36021 100644 --- a/test/tests/valid/enum.test.js +++ b/test/tests/valid/enum.test.js @@ -5,8 +5,8 @@ import { parseAttributes } from '../../utils.js'; function runTests(fileName) { - const isTS= fileName.endsWith('.ts'); - const script = isTS ? 'TypeScript' : 'JavaScript'; + const isTS = fileName.endsWith('.ts'); + const script = isTS ? 'TS' : 'JS'; describe(`${script}: VALID: Enum attribute`, function () { let data; diff --git a/test/tests/valid/json.test.js b/test/tests/valid/json.test.js index 79716f5..0f6baa9 100644 --- a/test/tests/valid/json.test.js +++ b/test/tests/valid/json.test.js @@ -3,7 +3,7 @@ import { describe, it, before } from 'mocha'; import { parseAttributes } from '../../utils.js'; -describe(`Javascript: VALID: Asset attribute`, function () { +describe('JS: VALID: Asset attribute', function () { let data; before(async function () { @@ -150,7 +150,7 @@ describe(`Javascript: VALID: Asset attribute`, function () { }); -describe(`Typescript: VALID: Asset attribute`, function () { +describe('TS: VALID: Asset attribute', function () { let data; before(async function () { From 830244c3c100025d5c8eef68341e144203bb9a78 Mon Sep 17 00:00:00 2001 From: Mark Lundin Date: Thu, 26 Sep 2024 09:45:44 +0100 Subject: [PATCH 3/3] linting --- src/utils/ts-utils.js | 6 +++--- test/fixtures/enum.valid.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/ts-utils.js b/src/utils/ts-utils.js index 392726f..56689e6 100644 --- a/src/utils/ts-utils.js +++ b/src/utils/ts-utils.js @@ -518,9 +518,9 @@ const resolvePropertyAccess = (node, typeChecker) => { if (ts.isVariableDeclaration(declaration) && declaration.initializer) { return getLiteralValue(declaration.initializer, typeChecker); } - - if(ts.isEnumMember(declaration)) { - return declaration.initializer ? getLiteralValue(declaration.initializer, typeChecker) : declaration.name.getText() + + if (ts.isEnumMember(declaration)) { + return declaration.initializer ? getLiteralValue(declaration.initializer, typeChecker) : declaration.name.getText(); } } diff --git a/test/fixtures/enum.valid.js b/test/fixtures/enum.valid.js index 89a3bd8..8c75042 100644 --- a/test/fixtures/enum.valid.js +++ b/test/fixtures/enum.valid.js @@ -12,6 +12,7 @@ const NumberEnum = { /** * @enum {string} */ +// eslint-disable-next-line const StringEnum = { A: 'a', B: 'b',