diff --git a/src/parsers/attribute-parser.js b/src/parsers/attribute-parser.js index 670aee0..f97190d 100644 --- a/src/parsers/attribute-parser.js +++ b/src/parsers/attribute-parser.js @@ -170,7 +170,7 @@ export class AttributeParser { const serializer = !array && this.typeSerializerMap.get(type); if (serializer) { try { - value = serializer(node.initializer ?? node); + value = serializer(node.initializer ?? node, this.typeChecker); } catch (error) { errors.push(error); return; diff --git a/src/parsers/script-parser.js b/src/parsers/script-parser.js index 6907b33..0197c0f 100644 --- a/src/parsers/script-parser.js +++ b/src/parsers/script-parser.js @@ -5,7 +5,7 @@ import { AttributeParser } from './attribute-parser.js'; import { ParsingError } from './parsing-error.js'; import { hasTag } from '../utils/attribute-utils.js'; import { zipArrays } from '../utils/generic-utils.js'; -import { flatMapAnyNodes, getJSDocCommentRanges, parseArrayLiteral, parseBooleanNode, parseFloatNode, parseStringNode } from '../utils/ts-utils.js'; +import { flatMapAnyNodes, getJSDocCommentRanges, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js'; /** * @typedef {object} Attribute @@ -77,9 +77,9 @@ const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([ ['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])], ['Vec4', createNumberArgumentParser('Vec4', [0, 0, 0, 0])], ['Color', createNumberArgumentParser('Color', [1, 1, 1, 1])], - ['number', parseFloatNode], - ['string', parseStringNode], - ['boolean', parseBooleanNode] + ['number', getLiteralValue], + ['string', getLiteralValue], + ['boolean', getLiteralValue] ]); /** @@ -187,6 +187,13 @@ const mapAttributesToOutput = (attribute) => { // remove enum if it's empty if (attribute.enum.length === 0) delete attribute.enum; + // If the attribute has no default value then set it + if (attribute.value === undefined) { + if (attribute.type === 'string') attribute.value = ''; + if (attribute.type === 'number') attribute.value = 0; + if (attribute.type === 'boolean') attribute.value = false; + } + // set the default value if (attribute.value !== undefined) attribute.default = attribute.value; diff --git a/src/utils/ts-utils.js b/src/utils/ts-utils.js index 833ebd2..93e8aee 100644 --- a/src/utils/ts-utils.js +++ b/src/utils/ts-utils.js @@ -461,6 +461,137 @@ export const parseBooleanNode = (node) => { return node.kind === ts.SyntaxKind.TrueKeyword; }; +function resolveIdentifier(node, typeChecker) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol && symbol.declarations) { + for (const declaration of symbol.declarations) { + if (ts.isVariableDeclaration(declaration) && declaration.initializer) { + return getLiteralValue(declaration.initializer, typeChecker); + } + // Handle other kinds of declarations if needed + } + } + return undefined; +} + +/** + * Resolve the value of a property access expression. Limited to simple cases like + * object literals and variable declarations. + * + * @param {import('typescript').Node} node - The property access expression node + * @param {import('typescript')} typeChecker - The TypeScript type checker + * @returns {any} - The resolved value of the property access + */ +const resolvePropertyAccess = (node, typeChecker) => { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol && symbol.declarations) { + for (const declaration of symbol.declarations) { + if (ts.isPropertyAssignment(declaration) && declaration.initializer) { + return getLiteralValue(declaration.initializer, typeChecker); + } + if (ts.isVariableDeclaration(declaration) && declaration.initializer) { + return getLiteralValue(declaration.initializer, typeChecker); + } + // Handle other kinds of declarations if needed + } + } + + // If symbol not found directly, attempt to resolve the object first + const objValue = getLiteralValue(node.expression, typeChecker); + if (objValue && typeof objValue === 'object') { + const propName = node.name.text; + return objValue[propName]; + } + + return undefined; +}; + +/** + * Evaluates unary prefixes like +, -, !, ~, and returns the result. + * @param {import('typescript').Node} node - The AST node to evaluate + * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker + * @returns {number | boolean | undefined} - The result of the evaluation + */ +const evaluatePrefixUnaryExpression = (node, typeChecker) => { + const operandValue = getLiteralValue(node.operand, typeChecker); + if (operandValue !== undefined) { + switch (node.operator) { + case ts.SyntaxKind.PlusToken: + return +operandValue; + case ts.SyntaxKind.MinusToken: + return -operandValue; + case ts.SyntaxKind.ExclamationToken: + return !operandValue; + case ts.SyntaxKind.TildeToken: + return ~operandValue; + } + } + return undefined; +}; + +function handleObjectLiteral(node, typeChecker) { + const obj = {}; + node.properties.forEach((prop) => { + if (ts.isPropertyAssignment(prop)) { + const key = prop.name.getText(); + const value = getLiteralValue(prop.initializer, typeChecker); + obj[key] = value; + } else if (ts.isShorthandPropertyAssignment(prop)) { + const key = prop.name.getText(); + const value = resolveIdentifier(prop.name, typeChecker); + obj[key] = value; + } + }); + return obj; +} + +/** + * Attempts to extract a literal value from a TypeScript node. This function + * supports various types of literals and expressions, including object literals, + * array literals, identifiers, and unary expressions. + * + * @param {import('typescript').Node} node - The AST node to evaluate + * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker + * @returns {any} - The extracted literal value + */ +export function getLiteralValue(node, typeChecker) { + if (!node) return undefined; + + if (ts.isLiteralExpression(node) || ts.isBooleanLiteral(node)) { + if (ts.isStringLiteral(node)) { + return node.text; + } + if (ts.isNumericLiteral(node)) { + return Number(node.text); + } + if (node.kind === ts.SyntaxKind.TrueKeyword) { + return true; + } + if (node.kind === ts.SyntaxKind.FalseKeyword) { + return false; + } + } + + switch (node.kind) { + case ts.SyntaxKind.NullKeyword: + return null; + case ts.SyntaxKind.ArrayLiteralExpression: + return (node).elements.map(element => getLiteralValue(element, typeChecker)); + case ts.SyntaxKind.ObjectLiteralExpression: + return handleObjectLiteral(node, typeChecker); + case ts.SyntaxKind.Identifier: + return resolveIdentifier(node, typeChecker); + case ts.SyntaxKind.PropertyAccessExpression: + return resolvePropertyAccess(node, typeChecker); + case ts.SyntaxKind.ParenthesizedExpression: + return getLiteralValue((node).expression, typeChecker); + case ts.SyntaxKind.PrefixUnaryExpression: + return evaluatePrefixUnaryExpression(node, typeChecker); + default: + return undefined; + } +} + /** * If the given node is a string literal, returns the parsed string. * @param {import('typescript').Node} node - The node to check diff --git a/test/fixtures/enum.valid.js b/test/fixtures/enum.valid.js index 049146d..251107f 100644 --- a/test/fixtures/enum.valid.js +++ b/test/fixtures/enum.valid.js @@ -44,7 +44,7 @@ class Example extends Script { * @attribute * @type {NumberEnum} */ - e; + e = NumberEnum.A; /** * @attribute