@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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
7
7
  },
8
- "version": "1.0.8",
8
+ "version": "1.1.0",
9
9
  "dependencies": {
10
10
  "@microsoft/tsdoc": "^0.15.0",
11
11
  "@playcanvas/eslint-config": "^1.7.4",
@@ -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, parseArrayLiteral, parseBooleanNode, parseFloatNode, parseStringNode } from '../utils/ts-utils.js';
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', parseFloatNode],
81
- ['string', parseStringNode],
82
- ['boolean', parseBooleanNode]
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
 
@@ -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 string representation of the value to type check
41
- * @param {string} typeAnnotation - The expected type
42
- * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment to validate in
43
- * @returns {boolean} - Whether the tag value is valid
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
- // Get the program and check for semantic errors
51
- const program = env.languageService.getProgram();
52
- const errors = program.getSemanticDiagnostics();
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
- // Filter against the type errors we're concerned with
55
- const typeErrors = errors.filter(error => error.category === 1 && error.code === 2322);
57
+ // Fetch semantic diagnostics only for the virtual file
58
+ const errors = languageService.getSemanticDiagnostics(virtualFileName);
56
59
 
57
- // return first error
58
- const typeError = typeErrors[0];
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
- if (typeError) {
61
- throw new Error(`${typeError.messageText}`);
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
  }
@@ -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
@@ -44,7 +44,7 @@ class Example extends Script {
44
44
  * @attribute
45
45
  * @type {NumberEnum}
46
46
  */
47
- e;
47
+ e = NumberEnum.A;
48
48
 
49
49
  /**
50
50
  * @attribute
@@ -8,13 +8,17 @@ export const MyEnum = { value: 0 };
8
8
  class Example extends Script {
9
9
  /**
10
10
  * @attribute
11
- * @type {boolean}
11
+ * @precision 1
12
+ * @type {number}
12
13
  */
13
- a = false;
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
 
@@ -13,5 +13,6 @@ describe('VALID: Program ', function () {
13
13
  expect(data).to.exist;
14
14
  expect(data[0]).to.not.be.empty;
15
15
  expect(data[1]).to.be.empty;
16
+ expect(data[0].example.errors).to.be.empty;
16
17
  });
17
18
  });