@playcanvas/attribute-parser 1.6.1 → 1.7.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/LICENSE.md +19 -0
- package/dist/index.js +194566 -0
- package/dist/libs.d.ts +50978 -0
- package/package.json +49 -9
- package/types/index.d.ts +55 -0
- package/types/parsers/attribute-parser.d.ts +55 -0
- package/types/parsers/parsing-error.d.ts +6 -0
- package/types/parsers/script-parser.d.ts +124 -0
- package/types/utils/attribute-utils.d.ts +5 -0
- package/{src/utils/generic-utils.js → types/utils/generic-utils.d.ts} +1 -10
- package/types/utils/tag-utils.d.ts +16 -0
- package/types/utils/ts-utils.d.ts +103 -0
- package/.github/workflows/ci.yml +0 -49
- package/eslint.config.mjs +0 -35
- package/renovate.json +0 -25
- package/src/parsers/attribute-parser.js +0 -197
- package/src/parsers/parsing-error.js +0 -14
- package/src/parsers/script-parser.js +0 -432
- package/src/utils/attribute-utils.js +0 -53
- package/src/utils/tag-utils.js +0 -73
- package/src/utils/ts-utils.js +0 -634
- package/test/fixtures/asset.invalid.js +0 -27
- package/test/fixtures/asset.invalid.ts +0 -24
- package/test/fixtures/asset.valid.js +0 -26
- package/test/fixtures/asset.valid.ts +0 -21
- package/test/fixtures/checkbox.valid.js +0 -23
- package/test/fixtures/color.valid.js +0 -23
- package/test/fixtures/conditional.valid.js +0 -22
- package/test/fixtures/curve.invalid.js +0 -29
- package/test/fixtures/curve.valid.js +0 -49
- package/test/fixtures/empty.valid.js +0 -0
- package/test/fixtures/entity.valid.js +0 -23
- package/test/fixtures/enum.valid.js +0 -66
- package/test/fixtures/enum.valid.ts +0 -50
- package/test/fixtures/example.dep.js +0 -9
- package/test/fixtures/export.import.js +0 -8
- package/test/fixtures/export.valid.js +0 -26
- package/test/fixtures/inherit.valid.js +0 -45
- package/test/fixtures/intellisense.valid.js +0 -40
- package/test/fixtures/json.invalid.js +0 -23
- package/test/fixtures/json.valid.js +0 -115
- package/test/fixtures/json.valid.ts +0 -81
- package/test/fixtures/misc.invalid.js +0 -24
- package/test/fixtures/misc.valid.js +0 -51
- package/test/fixtures/numeric.invalid.js +0 -38
- package/test/fixtures/numeric.valid.js +0 -63
- package/test/fixtures/playcanvas.d.ts +0 -47963
- package/test/fixtures/program.invalid.js +0 -12
- package/test/fixtures/program.valid.js +0 -27
- package/test/fixtures/text.valid.js +0 -31
- package/test/fixtures/vector.invalid.js +0 -14
- package/test/fixtures/vector.valid.js +0 -24
- package/test/tests/invalid/asset.test.js +0 -47
- package/test/tests/invalid/curve.test.js +0 -23
- package/test/tests/invalid/json.test.js +0 -27
- package/test/tests/invalid/misc.test.js +0 -41
- package/test/tests/invalid/numeric.test.js +0 -55
- package/test/tests/invalid/program.test.js +0 -17
- package/test/tests/invalid/vector.test.js +0 -28
- package/test/tests/valid/asset.test.js +0 -59
- package/test/tests/valid/checkbox.test.js +0 -57
- package/test/tests/valid/color.test.js +0 -49
- package/test/tests/valid/conditional.test.js +0 -45
- package/test/tests/valid/curve.test.js +0 -76
- package/test/tests/valid/empty.test.js +0 -17
- package/test/tests/valid/entity.test.js +0 -49
- package/test/tests/valid/enum.test.js +0 -79
- package/test/tests/valid/export.test.js +0 -67
- package/test/tests/valid/inherit.test.js +0 -110
- package/test/tests/valid/intellisense.test.js +0 -39
- package/test/tests/valid/json.test.js +0 -315
- package/test/tests/valid/misc.test.js +0 -54
- package/test/tests/valid/numeric.test.js +0 -103
- package/test/tests/valid/program.test.js +0 -23
- package/test/tests/valid/text.test.js +0 -58
- package/test/tests/valid/vector.test.js +0 -53
- package/test/utils.js +0 -54
package/src/utils/tag-utils.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { DiagnosticCategory } from 'typescript';
|
|
2
|
-
|
|
3
|
-
import { parseNumber, parseStringToNumericalArray } from './ts-utils.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parses a tag value into a boolean, number, array or string
|
|
7
|
-
* @param {string} input - The tag value to parse
|
|
8
|
-
* @returns {boolean|number|string|Array<any>} - The parsed value
|
|
9
|
-
*/
|
|
10
|
-
export function parseTag(input = '') {
|
|
11
|
-
|
|
12
|
-
// Remove any leading or trailing whitespace, replace single quotes with double quotes
|
|
13
|
-
input = input.replace(/['`]/g, '"').trim();
|
|
14
|
-
|
|
15
|
-
// Check for boolean
|
|
16
|
-
if (input === 'true') return true;
|
|
17
|
-
if (input === 'false') return false;
|
|
18
|
-
|
|
19
|
-
// Check for number
|
|
20
|
-
const num = parseNumber(input);
|
|
21
|
-
if (!isNaN(num)) return num;
|
|
22
|
-
|
|
23
|
-
// Check for numerical array
|
|
24
|
-
try {
|
|
25
|
-
const arr = parseStringToNumericalArray(input);
|
|
26
|
-
if (Array.isArray(arr)) return arr;
|
|
27
|
-
} catch { }
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const arr = JSON.parse(input);
|
|
31
|
-
if (Array.isArray(arr)) return arr;
|
|
32
|
-
} catch { }
|
|
33
|
-
|
|
34
|
-
// If none of the above, return the string
|
|
35
|
-
return input;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validates that a tag value matches the expected type
|
|
40
|
-
*
|
|
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.
|
|
46
|
-
*/
|
|
47
|
-
export function validateTag(value, typeAnnotation, env) {
|
|
48
|
-
const virtualFileName = '/___virtual__.ts';
|
|
49
|
-
const sourceText = `let a: ${typeAnnotation} = ${value};`;
|
|
50
|
-
|
|
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;
|
|
56
|
-
|
|
57
|
-
// Fetch semantic diagnostics only for the virtual file
|
|
58
|
-
const errors = languageService.getSemanticDiagnostics(virtualFileName);
|
|
59
|
-
|
|
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
|
-
);
|
|
64
|
-
|
|
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}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// If no type errors are found, return true indicating successful validation
|
|
72
|
-
return true;
|
|
73
|
-
}
|
package/src/utils/ts-utils.js
DELETED
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
import { knownLibFilesForCompilerOptions } from '@typescript/vfs';
|
|
2
|
-
import * as ts from 'typescript';
|
|
3
|
-
|
|
4
|
-
import { ParsingError } from '../parsers/parsing-error.js';
|
|
5
|
-
|
|
6
|
-
export function createDefaultMapFromCDN(options, prefix, ts) {
|
|
7
|
-
const fsMap = new Map();
|
|
8
|
-
const files = knownLibFilesForCompilerOptions(options, ts);
|
|
9
|
-
|
|
10
|
-
async function uncached() {
|
|
11
|
-
const contentPromises = files.map((lib) => {
|
|
12
|
-
return fetch(prefix + lib).then((resp) => {
|
|
13
|
-
if (!resp.ok) {
|
|
14
|
-
throw new Error(`Failed to fetch ${prefix + lib}: ${resp.statusText}`);
|
|
15
|
-
}
|
|
16
|
-
return resp.text();
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const contents = await Promise.all(contentPromises);
|
|
21
|
-
|
|
22
|
-
contents.forEach((text, index) => fsMap.set(`/${files[index]}`, text));
|
|
23
|
-
|
|
24
|
-
return fsMap;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return uncached();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolves the original symbol in case of alias.
|
|
32
|
-
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
33
|
-
* @param {import('typescript').Symbol} symbol - The TypeScript symbol
|
|
34
|
-
* @returns {import('typescript').Symbol} - The resolved symbol
|
|
35
|
-
*/
|
|
36
|
-
function resolveAliasedSymbol(typeChecker, symbol) {
|
|
37
|
-
while (symbol.flags & ts.SymbolFlags.Alias) {
|
|
38
|
-
symbol = typeChecker.getAliasedSymbol(symbol);
|
|
39
|
-
}
|
|
40
|
-
return symbol;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns an array of exported nodes from a TypeScript source file.
|
|
45
|
-
* @param {import('typescript').Program} program - The TypeScript program
|
|
46
|
-
* @param {import('typescript').SourceFile} sourceFile - The TypeScript source file
|
|
47
|
-
* @returns {let map: Map<string, Set<import('typescript').Node>>} - A Map of exported nodes
|
|
48
|
-
*/
|
|
49
|
-
export function getExportedNodes(program, sourceFile) {
|
|
50
|
-
if (!program || !sourceFile) {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const typeChecker = program.getTypeChecker();
|
|
55
|
-
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
|
|
56
|
-
|
|
57
|
-
if (!moduleSymbol) {
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const exportedSymbols = typeChecker.getExportsOfModule(moduleSymbol);
|
|
62
|
-
|
|
63
|
-
// Find the actual declaration nodes for each exported symbol
|
|
64
|
-
const exportedNodes = new Map();
|
|
65
|
-
|
|
66
|
-
exportedSymbols.forEach((symbol) => {
|
|
67
|
-
const resolvedSymbol = resolveAliasedSymbol(typeChecker, symbol);
|
|
68
|
-
if (resolvedSymbol.declarations) {
|
|
69
|
-
const node = resolvedSymbol.declarations[0];
|
|
70
|
-
const name = symbol.name === 'default' ? node.name.getText() : symbol.name;
|
|
71
|
-
exportedNodes.set(name, node);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return exportedNodes;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Checks if the node or its aliased symbol is a class declaration.
|
|
80
|
-
*
|
|
81
|
-
* @param {import('typescript').Node} node - The TypeScript node
|
|
82
|
-
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
83
|
-
* @returns {boolean} - True if the node or its original declaration is a class declaration, false otherwise
|
|
84
|
-
*/
|
|
85
|
-
export function isAliasedClassDeclaration(node, typeChecker) {
|
|
86
|
-
if (ts.isClassDeclaration(node)) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
// Try to resolve the original declaration
|
|
90
|
-
if (node?.name && ts.isIdentifier(node.name)) {
|
|
91
|
-
const symbol = typeChecker.getSymbolAtLocation(node.name);
|
|
92
|
-
if (symbol) {
|
|
93
|
-
const resolvedSymbol = resolveAliasedSymbol(typeChecker, symbol);
|
|
94
|
-
if (resolvedSymbol.declarations) {
|
|
95
|
-
return resolvedSymbol.declarations.some(ts.isClassDeclaration);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Determines if a TypeScript node inherits from a specific base class.
|
|
104
|
-
* @param {ts.ClassDeclaration} node - The TypeScript node to analyze
|
|
105
|
-
* @param {ts.TypeChecker} typeChecker - The TypeScript type checker
|
|
106
|
-
* @param {ts.Symbol} baseClassSymbol - The base class symbol to check inheritance against
|
|
107
|
-
* @returns {boolean} - True if the node inherits from the base class
|
|
108
|
-
*/
|
|
109
|
-
export function inheritsFrom(node, typeChecker, baseClassSymbol) {
|
|
110
|
-
if (node.heritageClauses) {
|
|
111
|
-
for (const clause of node.heritageClauses) {
|
|
112
|
-
for (const type of clause.types) {
|
|
113
|
-
// Get the symbol of the expression
|
|
114
|
-
const symbol = typeChecker.getTypeAtLocation(type.expression).getSymbol();
|
|
115
|
-
|
|
116
|
-
// Check if the symbol is the base class symbol
|
|
117
|
-
if (symbol === baseClassSymbol) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Get the declaration of the symbol to check its heritage clauses
|
|
122
|
-
if (symbol?.declarations.length > 0) {
|
|
123
|
-
const declaration = symbol.declarations[0];
|
|
124
|
-
if (inheritsFrom(declaration, typeChecker, baseClassSymbol)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Recursively gathers and flat maps and nodes that meet a specific filter.
|
|
136
|
-
* @param {import('typescript').Node} node - The TypeScript node to analyze
|
|
137
|
-
* @param {function} filter - A function to filter the nodes
|
|
138
|
-
* @returns {import('typescript').Node[]} - An array of exported class declarations
|
|
139
|
-
*/
|
|
140
|
-
export function flatMapAnyNodes(node, filter = () => true) {
|
|
141
|
-
const results = [];
|
|
142
|
-
|
|
143
|
-
function visit(node) {
|
|
144
|
-
// Check if this node is an exported class declaration
|
|
145
|
-
if (filter(node)) {
|
|
146
|
-
results.push(node);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Visit each child node
|
|
150
|
-
ts.forEachChild(node, visit);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Start the search with the root node
|
|
154
|
-
visit(node);
|
|
155
|
-
|
|
156
|
-
return results;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Takes a AST Node, and if it's a class declaration, it will return it's super classes
|
|
161
|
-
*
|
|
162
|
-
* @param {import('typescript').Node} node - The node to check
|
|
163
|
-
* @param {import('typescript').TypeChecker} typeChecker - The programs typechecker
|
|
164
|
-
* @returns {import('typescript').Node[]} - The analyzed node and any super classes in the inheritance chain
|
|
165
|
-
*/
|
|
166
|
-
function getSuperClasses(node, typeChecker) {
|
|
167
|
-
const superClasses = [node];
|
|
168
|
-
|
|
169
|
-
let currentClass = node;
|
|
170
|
-
|
|
171
|
-
while (currentClass) {
|
|
172
|
-
const heritageClauses = currentClass.heritageClauses;
|
|
173
|
-
if (!heritageClauses) {
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const extendsClause = heritageClauses.find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
178
|
-
if (!extendsClause || extendsClause.types.length === 0) {
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const superClassType = typeChecker.getTypeAtLocation(extendsClause.types[0]);
|
|
183
|
-
const superClassSymbol = superClassType.getSymbol();
|
|
184
|
-
|
|
185
|
-
if (!superClassSymbol) {
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const superClassDeclaration = superClassSymbol.declarations.find(ts.isClassDeclaration);
|
|
190
|
-
if (!superClassDeclaration) {
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
superClasses.push(superClassDeclaration);
|
|
195
|
-
currentClass = superClassDeclaration;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return superClasses; // .reverse(); // To return the array in order from the base class to the top-level class
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Retrieves the JSDoc-style comments associated with a specific AST node.
|
|
203
|
-
*
|
|
204
|
-
* @param {import('typescript').Node} node - The TypeScript node to analyze
|
|
205
|
-
* @param {string} text - The full text content of the source file
|
|
206
|
-
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
207
|
-
* @returns {{ memberName: string, member: Node, range: import('typescript').CommentRange}[]} - An array of comment ranges
|
|
208
|
-
*/
|
|
209
|
-
export function getJSDocTags(node, typeChecker) {
|
|
210
|
-
const tags = [];
|
|
211
|
-
|
|
212
|
-
// check if class interface of initialized object
|
|
213
|
-
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
214
|
-
// get an array of the class an all parent classed
|
|
215
|
-
const heritageChain = getSuperClasses(node, typeChecker)
|
|
216
|
-
.filter(classNode => !classNode.getSourceFile().isDeclarationFile);
|
|
217
|
-
|
|
218
|
-
// iterate over the heritance chain
|
|
219
|
-
heritageChain.forEach((classNode) => {
|
|
220
|
-
// for each class iterate over it's class members
|
|
221
|
-
classNode.members.forEach((member) => {
|
|
222
|
-
const isSetter = ts.isSetAccessor(member);
|
|
223
|
-
if (ts.isPropertyDeclaration(member) || isSetter || ts.isPropertySignature(member)) {
|
|
224
|
-
const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed';
|
|
225
|
-
|
|
226
|
-
let hasJsDoc = member.jsDoc?.length > 0;
|
|
227
|
-
|
|
228
|
-
// If we are a setter with no jsdoc block, check any associated getter
|
|
229
|
-
if (isSetter && !hasJsDoc) {
|
|
230
|
-
const symbol = typeChecker.getSymbolAtLocation(member.name);
|
|
231
|
-
member = symbol.declarations.find(accessor => accessor.jsDoc?.length > 0);
|
|
232
|
-
hasJsDoc = !!member;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (hasJsDoc) {
|
|
236
|
-
tags.push({
|
|
237
|
-
memberName,
|
|
238
|
-
member
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
} else if (ts.isObjectLiteralExpression(node)) {
|
|
245
|
-
// Handle object literals
|
|
246
|
-
node.properties.forEach((property) => {
|
|
247
|
-
if (ts.isPropertyAssignment(property)) {
|
|
248
|
-
const memberName = property.name &&
|
|
249
|
-
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ?
|
|
250
|
-
property.name.text :
|
|
251
|
-
'unnamed';
|
|
252
|
-
|
|
253
|
-
if (property.jsDoc?.length > 0) {
|
|
254
|
-
tags.push({
|
|
255
|
-
memberName,
|
|
256
|
-
member: property
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return tags;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Determines if a TypeScript node is an enum.
|
|
268
|
-
* @param {import('typescript').Node} node - The TypeScript node to analyze
|
|
269
|
-
* @returns {boolean} - True if the node is an enum
|
|
270
|
-
*/
|
|
271
|
-
export function isEnum(node) {
|
|
272
|
-
// Check for TypeScript native enum
|
|
273
|
-
if (ts.isEnumDeclaration(node)) {
|
|
274
|
-
return true; // The node is a TypeScript enum
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Check for JSDoc @enum tag in a variable declaration
|
|
278
|
-
if (ts.isVariableDeclaration(node)) {
|
|
279
|
-
const jsDocs = ts.getJSDocTags(node);
|
|
280
|
-
const hasEnumTag = jsDocs.some(doc => doc.tagName.text === 'enum');
|
|
281
|
-
return hasEnumTag;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Determines the primitive type for enums, or falls back to the actual type name.
|
|
289
|
-
*
|
|
290
|
-
* @param {ts.Type} type - The type to inspect.
|
|
291
|
-
* @param {ts.TypeChecker} typeChecker - The TypeScript type checker.
|
|
292
|
-
* @returns {'string' | 'boolean' | 'number' | null} - The primitive type of the enum or the type's name.
|
|
293
|
-
*/
|
|
294
|
-
export function getPrimitiveEnumType(type, typeChecker) {
|
|
295
|
-
// Check if the type is an enum type
|
|
296
|
-
if (!type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) return null;
|
|
297
|
-
|
|
298
|
-
// Get the type of enum members
|
|
299
|
-
const enumMembers = type.symbol.declarations[0].members;
|
|
300
|
-
const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]);
|
|
301
|
-
|
|
302
|
-
const validEnumType = [
|
|
303
|
-
'number',
|
|
304
|
-
'string',
|
|
305
|
-
'boolean'
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
const typeOf = typeof firstMemberValue;
|
|
309
|
-
return validEnumType.includes(typeOf) ? typeOf : null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Gets the inferred type of a TypeScript node.
|
|
314
|
-
*
|
|
315
|
-
* @param {ts.Node} node - The TypeScript node to analyze
|
|
316
|
-
* @param {ts.TypeChecker} typeChecker - The TypeScript type checker
|
|
317
|
-
* @returns {{ array: boolean, name: string | undefined, type: Type | null }} - The inferred type of the node
|
|
318
|
-
*/
|
|
319
|
-
export function getType(node, typeChecker) {
|
|
320
|
-
if (node?.name?.kind === ts.SyntaxKind.Identifier) {
|
|
321
|
-
const type = typeChecker.getTypeAtLocation(node);
|
|
322
|
-
const array = typeChecker.isArrayType(type);
|
|
323
|
-
const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type;
|
|
324
|
-
const name = getPrimitiveEnumType(actualType, typeChecker) ?? typeChecker.typeToString(actualType);
|
|
325
|
-
|
|
326
|
-
return { type: actualType, name, array };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Parses a numerical string into a number. Handles hexadecimal, binary,
|
|
334
|
-
* octal, exponential notation and underscore notation
|
|
335
|
-
*
|
|
336
|
-
* @param {string} input - The string to parse
|
|
337
|
-
* @returns {number} The parsed number, or NaN if the input is not a number
|
|
338
|
-
*/
|
|
339
|
-
export function parseNumber(input) {
|
|
340
|
-
// Remove underscores from the string to handle underscore notation
|
|
341
|
-
let cleanedInput = input.replace(/_/g, '').toLowerCase();
|
|
342
|
-
|
|
343
|
-
// Identify and isolate unary operators
|
|
344
|
-
let unaryOperator = null;
|
|
345
|
-
if (cleanedInput.startsWith('+') || cleanedInput.startsWith('-') || cleanedInput.startsWith('~')) {
|
|
346
|
-
unaryOperator = cleanedInput[0];
|
|
347
|
-
cleanedInput = cleanedInput.slice(1).trim();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Check for different bases and handle appropriately
|
|
351
|
-
let result;
|
|
352
|
-
if (cleanedInput === 'NaN' || cleanedInput === '') {
|
|
353
|
-
result = NaN;
|
|
354
|
-
} else if (cleanedInput.startsWith('0b')) {
|
|
355
|
-
result = parseInt(cleanedInput.slice(2), 2);
|
|
356
|
-
} else if (cleanedInput.startsWith('0o')) {
|
|
357
|
-
result = parseInt(cleanedInput.slice(2), 8);
|
|
358
|
-
} else if (cleanedInput.startsWith('0x')) {
|
|
359
|
-
result = parseInt(cleanedInput.slice(2), 16);
|
|
360
|
-
} else if (cleanedInput.includes('e')) {
|
|
361
|
-
result = parseFloat(cleanedInput); // Exponential notation
|
|
362
|
-
} else if (!isNaN(Number(cleanedInput))) {
|
|
363
|
-
result = Number(cleanedInput); // Decimal, including floating-point
|
|
364
|
-
} else {
|
|
365
|
-
result = NaN;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Apply unary operator if present
|
|
369
|
-
if (unaryOperator === '+') {
|
|
370
|
-
result = +result; // Positive sign can be ignored, but explicitly using for clarity
|
|
371
|
-
} else if (unaryOperator === '-') {
|
|
372
|
-
result = -result;
|
|
373
|
-
} else if (unaryOperator === '~') {
|
|
374
|
-
result = ~result; // Bitwise NOT
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Parses a string representation of a numerical array into an array of numbers.
|
|
382
|
-
*
|
|
383
|
-
* @param {string} input - The string to parse
|
|
384
|
-
* @returns {number[]} - The parsed array of numbers
|
|
385
|
-
* @throws {Error} - If the input contains invalid characters or is not a valid array
|
|
386
|
-
*/
|
|
387
|
-
export function parseStringToNumericalArray(input) {
|
|
388
|
-
// Remove unnecessary whitespace
|
|
389
|
-
const sanitizedInput = input.replace(/\s+/g, '');
|
|
390
|
-
|
|
391
|
-
// Check if the input still contains valid array format
|
|
392
|
-
if (!sanitizedInput.startsWith('[') || !sanitizedInput.endsWith(']')) {
|
|
393
|
-
throw new Error('Invalid input string');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Remove the brackets and split the string into an array
|
|
397
|
-
const arrayString = sanitizedInput.slice(1, -1);
|
|
398
|
-
const elements = arrayString.split(',');
|
|
399
|
-
|
|
400
|
-
// Parse each element using the parseNumber function
|
|
401
|
-
const parsedArray = elements.map((element) => {
|
|
402
|
-
return parseNumber(element.trim());
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Validate if the result is an array and contains only valid numbers
|
|
406
|
-
if (parsedArray.some(isNaN)) {
|
|
407
|
-
throw new Error('Parsed result contains invalid numbers');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return parsedArray;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Checks if the given node is a numerical literal or a unary expression.
|
|
415
|
-
* @param {import('typescript').Node} node - The node to check
|
|
416
|
-
* @returns {boolean} - True if the node is a numeric literal
|
|
417
|
-
*/
|
|
418
|
-
export const isNumericLiteral = (node) => {
|
|
419
|
-
// Is the node a numeric literal '10'
|
|
420
|
-
const isNumericLiteral = ts.isNumericLiteral(node);
|
|
421
|
-
|
|
422
|
-
// Or is it a +/- unary expression '-10' or '+10'
|
|
423
|
-
const isUnaryExpression =
|
|
424
|
-
ts.isUnaryExpression(node) && //
|
|
425
|
-
(node.operator === ts.SyntaxKind.MinusToken || node.operator === ts.SyntaxKind.PlusToken) &&
|
|
426
|
-
node.operand.kind === ts.SyntaxKind.NumericLiteral;
|
|
427
|
-
|
|
428
|
-
return isNumericLiteral || isUnaryExpression;
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* If the given node is a numeric literal, returns the parsed number.
|
|
433
|
-
* @param {import('typescript').Node} node - The node to check
|
|
434
|
-
* @param {boolean} [isArgument] - True if the node is an argument in a parameter list
|
|
435
|
-
* @returns {number} - The parsed number
|
|
436
|
-
* @throws {ParsingError} - If the node is not a numeric literal
|
|
437
|
-
*/
|
|
438
|
-
export const parseFloatNode = (node, isArgument = false) => {
|
|
439
|
-
if (!isNumericLiteral(node)) {
|
|
440
|
-
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) {
|
|
441
|
-
throw new ParsingError(node, `"${node.parent.getText()}" must be defined with a numeric literal.`);
|
|
442
|
-
}
|
|
443
|
-
return 0;
|
|
444
|
-
}
|
|
445
|
-
return parseNumber(node.getText());
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* If the given node is a boolean literal, returns the parsed boolean.
|
|
450
|
-
* @param {import('typescript').Node} node - The node to check
|
|
451
|
-
* @returns {boolean} - The parsed boolean
|
|
452
|
-
*/
|
|
453
|
-
export const parseBooleanNode = (node) => {
|
|
454
|
-
if (node.kind !== ts.SyntaxKind.TrueKeyword && node.kind !== ts.SyntaxKind.FalseKeyword) {
|
|
455
|
-
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
|
|
456
|
-
throw new ParsingError(node, `"${node.parent.getText()}" must be defined with a boolean literal.`);
|
|
457
|
-
}
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
return node.kind === ts.SyntaxKind.TrueKeyword;
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
function resolveIdentifier(node, typeChecker) {
|
|
464
|
-
const symbol = typeChecker.getSymbolAtLocation(node);
|
|
465
|
-
if (symbol && symbol.declarations) {
|
|
466
|
-
for (const declaration of symbol.declarations) {
|
|
467
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
468
|
-
return getLiteralValue(declaration.initializer, typeChecker);
|
|
469
|
-
}
|
|
470
|
-
// Handle other kinds of declarations if needed
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Resolve the value of a property access expression. Limited to simple cases like
|
|
478
|
-
* object literals and variable declarations.
|
|
479
|
-
*
|
|
480
|
-
* @param {import('typescript').Node} node - The property access expression node
|
|
481
|
-
* @param {import('typescript')} typeChecker - The TypeScript type checker
|
|
482
|
-
* @returns {any} - The resolved value of the property access
|
|
483
|
-
*/
|
|
484
|
-
const resolvePropertyAccess = (node, typeChecker) => {
|
|
485
|
-
const symbol = typeChecker.getSymbolAtLocation(node);
|
|
486
|
-
if (symbol && symbol.declarations) {
|
|
487
|
-
for (const declaration of symbol.declarations) {
|
|
488
|
-
if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
|
|
489
|
-
return getLiteralValue(declaration.initializer, typeChecker);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
493
|
-
return getLiteralValue(declaration.initializer, typeChecker);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (ts.isEnumMember(declaration)) {
|
|
497
|
-
return declaration.initializer ? getLiteralValue(declaration.initializer, typeChecker) : declaration.name.getText();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// If symbol not found directly, attempt to resolve the object first
|
|
504
|
-
const objValue = getLiteralValue(node.expression, typeChecker);
|
|
505
|
-
if (objValue && typeof objValue === 'object') {
|
|
506
|
-
const propName = node.name.text;
|
|
507
|
-
return objValue[propName];
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return undefined;
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Evaluates unary prefixes like +, -, !, ~, and returns the result.
|
|
515
|
-
* @param {import('typescript').Node} node - The AST node to evaluate
|
|
516
|
-
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
517
|
-
* @returns {number | boolean | undefined} - The result of the evaluation
|
|
518
|
-
*/
|
|
519
|
-
const evaluatePrefixUnaryExpression = (node, typeChecker) => {
|
|
520
|
-
const operandValue = getLiteralValue(node.operand, typeChecker);
|
|
521
|
-
if (operandValue !== undefined) {
|
|
522
|
-
switch (node.operator) {
|
|
523
|
-
case ts.SyntaxKind.PlusToken:
|
|
524
|
-
return +operandValue;
|
|
525
|
-
case ts.SyntaxKind.MinusToken:
|
|
526
|
-
return -operandValue;
|
|
527
|
-
case ts.SyntaxKind.ExclamationToken:
|
|
528
|
-
return !operandValue;
|
|
529
|
-
case ts.SyntaxKind.TildeToken:
|
|
530
|
-
return ~operandValue;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return undefined;
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
function handleObjectLiteral(node, typeChecker) {
|
|
537
|
-
const obj = {};
|
|
538
|
-
node.properties.forEach((prop) => {
|
|
539
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
540
|
-
const key = prop.name.getText();
|
|
541
|
-
const value = getLiteralValue(prop.initializer, typeChecker);
|
|
542
|
-
obj[key] = value;
|
|
543
|
-
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
544
|
-
const key = prop.name.getText();
|
|
545
|
-
const value = resolveIdentifier(prop.name, typeChecker);
|
|
546
|
-
obj[key] = value;
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
return obj;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Attempts to extract a literal value from a TypeScript node. This function
|
|
554
|
-
* supports various types of literals and expressions, including object literals,
|
|
555
|
-
* array literals, identifiers, and unary expressions.
|
|
556
|
-
*
|
|
557
|
-
* @param {import('typescript').Node} node - The AST node to evaluate
|
|
558
|
-
* @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
|
|
559
|
-
* @returns {any} - The extracted literal value
|
|
560
|
-
*/
|
|
561
|
-
export function getLiteralValue(node, typeChecker) {
|
|
562
|
-
if (!node) return undefined;
|
|
563
|
-
|
|
564
|
-
if (ts.isLiteralExpression(node) || ts.isBooleanLiteral(node)) {
|
|
565
|
-
if (ts.isStringLiteral(node)) {
|
|
566
|
-
return node.text;
|
|
567
|
-
}
|
|
568
|
-
if (ts.isNumericLiteral(node)) {
|
|
569
|
-
return Number(node.text);
|
|
570
|
-
}
|
|
571
|
-
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
switch (node.kind) {
|
|
580
|
-
case ts.SyntaxKind.NullKeyword:
|
|
581
|
-
return null;
|
|
582
|
-
case ts.SyntaxKind.ArrayLiteralExpression:
|
|
583
|
-
return (node).elements.map(element => getLiteralValue(element, typeChecker));
|
|
584
|
-
case ts.SyntaxKind.ObjectLiteralExpression:
|
|
585
|
-
return handleObjectLiteral(node, typeChecker);
|
|
586
|
-
case ts.SyntaxKind.Identifier:
|
|
587
|
-
return resolveIdentifier(node, typeChecker);
|
|
588
|
-
case ts.SyntaxKind.PropertyAccessExpression:
|
|
589
|
-
return resolvePropertyAccess(node, typeChecker);
|
|
590
|
-
case ts.SyntaxKind.ParenthesizedExpression:
|
|
591
|
-
return getLiteralValue((node).expression, typeChecker);
|
|
592
|
-
case ts.SyntaxKind.PrefixUnaryExpression:
|
|
593
|
-
return evaluatePrefixUnaryExpression(node, typeChecker);
|
|
594
|
-
default:
|
|
595
|
-
return undefined;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* If the given node is a string literal, returns the parsed string.
|
|
601
|
-
* @param {import('typescript').Node} node - The node to check
|
|
602
|
-
* @returns {string} - The parsed string
|
|
603
|
-
*/
|
|
604
|
-
export const parseStringNode = (node) => {
|
|
605
|
-
if (node.kind !== ts.SyntaxKind.StringLiteral) {
|
|
606
|
-
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
|
|
607
|
-
throw new ParsingError(node, `"${node.parent.getText()}" must be defined with a string literal.`);
|
|
608
|
-
}
|
|
609
|
-
return '';
|
|
610
|
-
}
|
|
611
|
-
return node.getText().slice(1, -1);
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
export const parseArrayLiteralNode = (node, isArgument = false) => {
|
|
615
|
-
if (node.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
|
|
616
|
-
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) {
|
|
617
|
-
throw new ParsingError(node, `"${node.parent.getText()}" must be defined with an array literal.`);
|
|
618
|
-
}
|
|
619
|
-
return [];
|
|
620
|
-
}
|
|
621
|
-
return JSON.parse(node.getText());
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Parses the first argument as an array literal.
|
|
626
|
-
* @param {ts.Node[]} args - The arguments to parse
|
|
627
|
-
* @returns {number[]} - The parsed array
|
|
628
|
-
*/
|
|
629
|
-
export const parseArrayLiteral = (args) => {
|
|
630
|
-
if (args.length > 0) {
|
|
631
|
-
return parseArrayLiteralNode(args[0], true);
|
|
632
|
-
}
|
|
633
|
-
return [];
|
|
634
|
-
};
|