Skip to content

Commit c1bad7b

Browse files
marklundinCopilot
andauthored
Support Literal Unions (#50)
* Enhance attribute parsing to handle mixed literal union types - Updated `getType` function to identify mixed literal unions and return an `isMixedUnion` flag. - Modified `getNodeAsAttribute` to raise errors for unsupported mixed literal union types. - Added tests for both valid and invalid mixed literal unions to ensure proper error handling. - Introduced new fixture files for testing mixed literal unions. * Update src/parsers/attribute-parser.js Co-authored-by: Copilot <[email protected]> * Fix test execution by uncommenting the call to runTests for enum.valid.js * Refactor attribute parser error message and enhance mixed union detection - Updated error message formatting in `attribute-parser.js` for clarity. - Improved logic in `getType` function in `ts-utils.js` to accurately identify mixed literal unions. --------- Co-authored-by: Copilot <[email protected]>
1 parent a03bb37 commit c1bad7b

File tree

6 files changed

+161
-3
lines changed

6 files changed

+161
-3
lines changed

src/parsers/attribute-parser.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,22 @@ export class AttributeParser {
8989
getNodeAsAttribute(node, errors = []) {
9090

9191
const name = node.name && ts.isIdentifier(node.name) && node.name.text;
92-
const { type, name: typeName, array } = getType(node, this.typeChecker);
92+
const { type, name: typeName, array, isMixedUnion } = getType(node, this.typeChecker);
93+
9394
const enums = this.getEnumMembers(node, errors);
9495
let value = null;
9596

97+
// If this is not an enum and the type is a mixed union, ie 'a' | false | 1,
98+
// we need to raise an error as this is not a supported attribute type
99+
if (isMixedUnion && enums.length === 0) {
100+
errors.push(new ParsingError(
101+
node,
102+
`Mixed literal union types (combining different primitive types like string | number) are not supported for attribute: '${name}'. ` +
103+
'Please use a union of the same primitive type (e.g., \'1 | 2 | 3\' or \'"a" | "b" | "c"\') or refactor your type.'
104+
));
105+
return;
106+
}
107+
96108
// we don't need to serialize the value for arrays
97109
const serializer = !array && this.typeSerializerMap.get(typeName);
98110
if (serializer) {

src/utils/ts-utils.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,23 @@ export function getType(node, typeChecker) {
321321
const type = typeChecker.getTypeAtLocation(node);
322322
const array = typeChecker.isArrayType(type);
323323
const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type;
324-
const name = getPrimitiveEnumType(actualType, typeChecker) ?? typeChecker.typeToString(actualType);
325324

326-
return { type: actualType, name, array };
325+
// A type could be a literal union, such as '1 | 2 | 3', so we need to get the base type
326+
const baseType = typeChecker.getBaseTypeOfLiteralType(actualType);
327+
328+
// If baseType has multiple types, it means we have a mixed union, ie 'a' | false | 1.
329+
let isMixedUnion = false;
330+
if (Array.isArray(baseType.types)) {
331+
const primitiveKinds = new Set(
332+
baseType.types.map(t => typeChecker.getBaseTypeOfLiteralType(t))
333+
);
334+
primitiveKinds.delete(null);
335+
isMixedUnion = primitiveKinds.size > 1;
336+
}
337+
338+
const name = getPrimitiveEnumType(baseType, typeChecker) ?? typeChecker.typeToString(baseType);
339+
340+
return { type: baseType, name, array, isMixedUnion };
327341
}
328342

329343
return null;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Script } from 'playcanvas';
2+
3+
class Example extends Script {
4+
/**
5+
* @attribute
6+
* @type {1 | 'one' | true}
7+
*/
8+
mixedLiteralUnion;
9+
10+
/**
11+
* @attribute
12+
* @type {'a' | 1 | false}
13+
*/
14+
mixedStringNumberBoolean;
15+
16+
/**
17+
* @attribute
18+
* @type {string | number | boolean}
19+
*/
20+
mixedPrimitiveTypes;
21+
}
22+
23+
export { Example };

test/fixtures/literal-unions.valid.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Script } from 'playcanvas';
2+
3+
class Example extends Script {
4+
/**
5+
* @attribute
6+
* @type {'a' | 'b' | 'c'}
7+
*/
8+
stringUnion;
9+
10+
/**
11+
* @attribute
12+
* @type {1 | 2 | 3}
13+
*/
14+
numericUnion;
15+
16+
/**
17+
* @attribute
18+
* @type {true | false}
19+
*/
20+
booleanUnion;
21+
}
22+
23+
export { Example };
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect } from 'chai';
2+
import { describe, it, before } from 'mocha';
3+
4+
import { parseAttributes } from '../../utils.js';
5+
6+
describe('INVALID: Mixed literal union types', function () {
7+
let data;
8+
before(async function () {
9+
data = await parseAttributes('./literal-unions.invalid.js');
10+
});
11+
12+
it('should have errors for mixed literal unions', function () {
13+
expect(data).to.exist;
14+
expect(data[0].example).to.exist;
15+
expect(data[1]).to.be.empty;
16+
expect(data[0].example.errors).to.not.be.empty; // errors array should not be empty
17+
});
18+
19+
it('mixedLiteralUnion: should raise an error for mixed literal union', function () {
20+
const errors = data[0].example.errors;
21+
const mixedUnionError = errors.find(error => error.message.includes('mixedLiteralUnion') ||
22+
error.message.includes('mixed literal union')
23+
);
24+
expect(mixedUnionError).to.exist;
25+
});
26+
27+
it('mixedStringNumberBoolean: should raise an error for mixed string/number/boolean union', function () {
28+
const errors = data[0].example.errors;
29+
const mixedUnionError = errors.find(error => error.message.includes('mixedStringNumberBoolean') ||
30+
error.message.includes('mixed literal union')
31+
);
32+
expect(mixedUnionError).to.exist;
33+
});
34+
35+
it('mixedPrimitiveTypes: should raise an error for mixed primitive types union', function () {
36+
const errors = data[0].example.errors;
37+
const mixedUnionError = errors.find(error => error.message.includes('mixedPrimitiveTypes') ||
38+
error.message.includes('mixed literal union')
39+
);
40+
expect(mixedUnionError).to.exist;
41+
});
42+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { expect } from 'chai';
2+
import { describe, it, before } from 'mocha';
3+
4+
import { parseAttributes } from '../../utils.js';
5+
6+
describe('VALID: Literal union types', function () {
7+
let data;
8+
before(async function () {
9+
data = await parseAttributes('./literal-unions.valid.js');
10+
});
11+
12+
it('only results should exist', function () {
13+
expect(data).to.exist;
14+
expect(data[0]).to.not.be.empty;
15+
expect(data[1]).to.be.empty;
16+
});
17+
18+
it('Example: should exist without errors', function () {
19+
expect(data[0]?.example).to.exist;
20+
expect(data[0].example.attributes).to.not.be.empty;
21+
expect(data[0].example.errors).to.be.empty;
22+
});
23+
24+
it('stringUnion: should be a string attribute from literal union', function () {
25+
expect(data[0].example.attributes.stringUnion).to.exist;
26+
expect(data[0].example.attributes.stringUnion.name).to.equal('stringUnion');
27+
expect(data[0].example.attributes.stringUnion.type).to.equal('string');
28+
expect(data[0].example.attributes.stringUnion.array).to.equal(false);
29+
});
30+
31+
it('numericUnion: should be a number attribute from literal union', function () {
32+
expect(data[0].example.attributes.numericUnion).to.exist;
33+
expect(data[0].example.attributes.numericUnion.name).to.equal('numericUnion');
34+
expect(data[0].example.attributes.numericUnion.type).to.equal('number');
35+
expect(data[0].example.attributes.numericUnion.array).to.equal(false);
36+
});
37+
38+
it('booleanUnion: should be a boolean attribute from literal union', function () {
39+
expect(data[0].example.attributes.booleanUnion).to.exist;
40+
expect(data[0].example.attributes.booleanUnion.name).to.equal('booleanUnion');
41+
expect(data[0].example.attributes.booleanUnion.type).to.equal('boolean');
42+
expect(data[0].example.attributes.booleanUnion.array).to.equal(false);
43+
});
44+
});

0 commit comments

Comments
 (0)