@playcanvas/attribute-parser 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/parsers/attribute-parser.js +1 -1
- package/src/parsers/script-parser.js +11 -4
- package/src/utils/tag-utils.js +24 -16
- package/src/utils/ts-utils.js +131 -0
- package/test/fixtures/enum.valid.js +1 -1
- package/test/fixtures/program.valid.js +6 -2
- package/test/tests/valid/program.test.js +1 -0
package/package.json
CHANGED
|
@@ -170,7 +170,7 @@ export class AttributeParser {
|
|
|
170
170
|
const serializer = !array && this.typeSerializerMap.get(type);
|
|
171
171
|
if (serializer) {
|
|
172
172
|
try {
|
|
173
|
-
value = serializer(node.initializer ?? node);
|
|
173
|
+
value = serializer(node.initializer ?? node, this.typeChecker);
|
|
174
174
|
} catch (error) {
|
|
175
175
|
errors.push(error);
|
|
176
176
|
return;
|
|
@@ -5,7 +5,7 @@ import { AttributeParser } from './attribute-parser.js';
|
|
|
5
5
|
import { ParsingError } from './parsing-error.js';
|
|
6
6
|
import { hasTag } from '../utils/attribute-utils.js';
|
|
7
7
|
import { zipArrays } from '../utils/generic-utils.js';
|
|
8
|
-
import { flatMapAnyNodes, getJSDocCommentRanges,
|
|
8
|
+
import { flatMapAnyNodes, getJSDocCommentRanges, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {object} Attribute
|
|
@@ -77,9 +77,9 @@ const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([
|
|
|
77
77
|
['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])],
|
|
78
78
|
['Vec4', createNumberArgumentParser('Vec4', [0, 0, 0, 0])],
|
|
79
79
|
['Color', createNumberArgumentParser('Color', [1, 1, 1, 1])],
|
|
80
|
-
['number',
|
|
81
|
-
['string',
|
|
82
|
-
['boolean',
|
|
80
|
+
['number', getLiteralValue],
|
|
81
|
+
['string', getLiteralValue],
|
|
82
|
+
['boolean', getLiteralValue]
|
|
83
83
|
]);
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -187,6 +187,13 @@ const mapAttributesToOutput = (attribute) => {
|
|
|
187
187
|
// remove enum if it's empty
|
|
188
188
|
if (attribute.enum.length === 0) delete attribute.enum;
|
|
189
189
|
|
|
190
|
+
// If the attribute has no default value then set it
|
|
191
|
+
if (attribute.value === undefined) {
|
|
192
|
+
if (attribute.type === 'string') attribute.value = '';
|
|
193
|
+
if (attribute.type === 'number') attribute.value = 0;
|
|
194
|
+
if (attribute.type === 'boolean') attribute.value = false;
|
|
195
|
+
}
|
|
196
|
+
|
|
190
197
|
// set the default value
|
|
191
198
|
if (attribute.value !== undefined) attribute.default = attribute.value;
|
|
192
199
|
|
package/src/utils/tag-utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DiagnosticCategory } from 'typescript';
|
|
2
|
+
|
|
1
3
|
import { parseNumber, parseStringToNumericalArray } from './ts-utils.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -35,31 +37,37 @@ export function parseTag(input = '') {
|
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Validates that a tag value matches the expected type
|
|
38
|
-
* @throws {Error} - If the tag value is not the expected type
|
|
39
40
|
*
|
|
40
|
-
* @param {String} value - The
|
|
41
|
-
* @param {
|
|
42
|
-
* @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment
|
|
43
|
-
* @
|
|
41
|
+
* @param {String} value - The value to be validated.
|
|
42
|
+
* @param {String} typeAnnotation - The TypeScript type annotation as a string.
|
|
43
|
+
* @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment containing the language service and file creation utilities.
|
|
44
|
+
* @throws Will throw an error if the value does not conform to the typeAnnotation.
|
|
45
|
+
* @returns {true} if validation passes without type errors.
|
|
44
46
|
*/
|
|
45
47
|
export function validateTag(value, typeAnnotation, env) {
|
|
46
|
-
|
|
48
|
+
const virtualFileName = '/___virtual__.ts';
|
|
47
49
|
const sourceText = `let a: ${typeAnnotation} = ${value};`;
|
|
48
|
-
env.createFile('/___virtual__.ts', sourceText);
|
|
49
50
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Create or overwrite the virtual file with the new source text
|
|
52
|
+
env.createFile(virtualFileName, sourceText);
|
|
53
|
+
|
|
54
|
+
// Retrieve the language service from the environment
|
|
55
|
+
const languageService = env.languageService;
|
|
53
56
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
57
|
+
// Fetch semantic diagnostics only for the virtual file
|
|
58
|
+
const errors = languageService.getSemanticDiagnostics(virtualFileName);
|
|
56
59
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
60
|
+
// Filter for type assignment errors (Error Code 2322: Type 'X' is not assignable to type 'Y')
|
|
61
|
+
const typeErrors = errors.filter(
|
|
62
|
+
error => error.code === 2322 && error.category === DiagnosticCategory.Error
|
|
63
|
+
);
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
// If any type error is found, throw an error with the diagnostic message
|
|
66
|
+
if (typeErrors.length > 0) {
|
|
67
|
+
const errorMessage = typeErrors[0].messageText.toString();
|
|
68
|
+
throw new Error(`Type Validation Error: ${errorMessage}`);
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
// If no type errors are found, return true indicating successful validation
|
|
64
72
|
return true;
|
|
65
73
|
}
|
package/src/utils/ts-utils.js
CHANGED
|
@@ -461,6 +461,137 @@ export const parseBooleanNode = (node) => {
|
|
|
461
461
|
return node.kind === ts.SyntaxKind.TrueKeyword;
|
|
462
462
|
};
|
|
463
463
|
|
|
464
|
+
function resolveIdentifier(node, typeChecker) {
|
|
465
|
+
const symbol = typeChecker.getSymbolAtLocation(node);
|
|
466
|
+
if (symbol && symbol.declarations) {
|
|
467
|
+
for (const declaration of symbol.declarations) {
|
|
468
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
469
|
+
return getLiteralValue(declaration.initializer, typeChecker);
|
|
470
|
+
}
|
|
471
|
+
// Handle other kinds of declarations if needed
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Resolve the value of a property access expression. Limited to simple cases like
|
|
479
|
+
* object literals and variable declarations.
|
|
480
|
+
*
|
|
481
|
+
* @param {import('typescript').Node} node - The property access expression node
|
|
482
|
+
* @param {import('typescript')} typeChecker - The TypeScript type checker
|
|
483
|
+
* @returns {any} - The resolved value of the property access
|
|
484
|
+
*/
|
|
485
|
+
const resolvePropertyAccess = (node, typeChecker) => {
|
|
486
|
+
const symbol = typeChecker.getSymbolAtLocation(node);
|
|
487
|
+
if (symbol && symbol.declarations) {
|
|
488
|
+
for (const declaration of symbol.declarations) {
|
|
489
|
+
if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
|
|
490
|
+
return getLiteralValue(declaration.initializer, typeChecker);
|
|
491
|
+
}
|
|
492
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
493
|
+
return getLiteralValue(declaration.initializer, typeChecker);
|
|
494
|
+
}
|
|
495
|
+
// Handle other kinds of declarations if needed
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// If symbol not found directly, attempt to resolve the object first
|
|
500
|
+
const objValue = getLiteralValue(node.expression, typeChecker);
|
|
501
|
+
if (objValue && typeof objValue === 'object') {
|
|
502
|
+
const propName = node.name.text;
|
|
503
|
+
return objValue[propName];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return undefined;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Evaluates unary prefixes like +, -, !, ~, and returns the result.
|
|
511
|
+
* @param {import('typescript').Node} node - The AST node to evaluate
|
|
512
|
+
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
513
|
+
* @returns {number | boolean | undefined} - The result of the evaluation
|
|
514
|
+
*/
|
|
515
|
+
const evaluatePrefixUnaryExpression = (node, typeChecker) => {
|
|
516
|
+
const operandValue = getLiteralValue(node.operand, typeChecker);
|
|
517
|
+
if (operandValue !== undefined) {
|
|
518
|
+
switch (node.operator) {
|
|
519
|
+
case ts.SyntaxKind.PlusToken:
|
|
520
|
+
return +operandValue;
|
|
521
|
+
case ts.SyntaxKind.MinusToken:
|
|
522
|
+
return -operandValue;
|
|
523
|
+
case ts.SyntaxKind.ExclamationToken:
|
|
524
|
+
return !operandValue;
|
|
525
|
+
case ts.SyntaxKind.TildeToken:
|
|
526
|
+
return ~operandValue;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
function handleObjectLiteral(node, typeChecker) {
|
|
533
|
+
const obj = {};
|
|
534
|
+
node.properties.forEach((prop) => {
|
|
535
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
536
|
+
const key = prop.name.getText();
|
|
537
|
+
const value = getLiteralValue(prop.initializer, typeChecker);
|
|
538
|
+
obj[key] = value;
|
|
539
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
540
|
+
const key = prop.name.getText();
|
|
541
|
+
const value = resolveIdentifier(prop.name, typeChecker);
|
|
542
|
+
obj[key] = value;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
return obj;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Attempts to extract a literal value from a TypeScript node. This function
|
|
550
|
+
* supports various types of literals and expressions, including object literals,
|
|
551
|
+
* array literals, identifiers, and unary expressions.
|
|
552
|
+
*
|
|
553
|
+
* @param {import('typescript').Node} node - The AST node to evaluate
|
|
554
|
+
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
555
|
+
* @returns {any} - The extracted literal value
|
|
556
|
+
*/
|
|
557
|
+
export function getLiteralValue(node, typeChecker) {
|
|
558
|
+
if (!node) return undefined;
|
|
559
|
+
|
|
560
|
+
if (ts.isLiteralExpression(node) || ts.isBooleanLiteral(node)) {
|
|
561
|
+
if (ts.isStringLiteral(node)) {
|
|
562
|
+
return node.text;
|
|
563
|
+
}
|
|
564
|
+
if (ts.isNumericLiteral(node)) {
|
|
565
|
+
return Number(node.text);
|
|
566
|
+
}
|
|
567
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
switch (node.kind) {
|
|
576
|
+
case ts.SyntaxKind.NullKeyword:
|
|
577
|
+
return null;
|
|
578
|
+
case ts.SyntaxKind.ArrayLiteralExpression:
|
|
579
|
+
return (node).elements.map(element => getLiteralValue(element, typeChecker));
|
|
580
|
+
case ts.SyntaxKind.ObjectLiteralExpression:
|
|
581
|
+
return handleObjectLiteral(node, typeChecker);
|
|
582
|
+
case ts.SyntaxKind.Identifier:
|
|
583
|
+
return resolveIdentifier(node, typeChecker);
|
|
584
|
+
case ts.SyntaxKind.PropertyAccessExpression:
|
|
585
|
+
return resolvePropertyAccess(node, typeChecker);
|
|
586
|
+
case ts.SyntaxKind.ParenthesizedExpression:
|
|
587
|
+
return getLiteralValue((node).expression, typeChecker);
|
|
588
|
+
case ts.SyntaxKind.PrefixUnaryExpression:
|
|
589
|
+
return evaluatePrefixUnaryExpression(node, typeChecker);
|
|
590
|
+
default:
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
464
595
|
/**
|
|
465
596
|
* If the given node is a string literal, returns the parsed string.
|
|
466
597
|
* @param {import('typescript').Node} node - The node to check
|
|
@@ -8,13 +8,17 @@ export const MyEnum = { value: 0 };
|
|
|
8
8
|
class Example extends Script {
|
|
9
9
|
/**
|
|
10
10
|
* @attribute
|
|
11
|
-
* @
|
|
11
|
+
* @precision 1
|
|
12
|
+
* @type {number}
|
|
12
13
|
*/
|
|
13
|
-
a
|
|
14
|
+
a;
|
|
14
15
|
|
|
15
16
|
initialize() {
|
|
16
17
|
confetti();
|
|
17
18
|
new TWEEN.Tween({ x: 0 }).to({ x: 100 }, 1000).start();
|
|
19
|
+
|
|
20
|
+
// This is an intentional type error, but the parser should ignore these
|
|
21
|
+
this.a = 'string';
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|