@playcanvas/attribute-parser 1.0.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.
Files changed (56) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/README.md +76 -0
  3. package/package.json +24 -0
  4. package/src/index.js +226 -0
  5. package/src/parsers/attribute-parser.js +280 -0
  6. package/src/parsers/parsing-error.js +14 -0
  7. package/src/parsers/script-parser.js +404 -0
  8. package/src/utils/attribute-utils.js +72 -0
  9. package/src/utils/generic-utils.js +18 -0
  10. package/src/utils/tag-utils.js +65 -0
  11. package/src/utils/ts-utils.js +501 -0
  12. package/test/fixtures/asset.invalid.js +26 -0
  13. package/test/fixtures/asset.valid.js +25 -0
  14. package/test/fixtures/checkbox.valid.js +23 -0
  15. package/test/fixtures/color.valid.js +23 -0
  16. package/test/fixtures/curve.invalid.js +28 -0
  17. package/test/fixtures/curve.valid.js +49 -0
  18. package/test/fixtures/entity.valid.js +23 -0
  19. package/test/fixtures/enum.valid.js +78 -0
  20. package/test/fixtures/export.import.js +3 -0
  21. package/test/fixtures/export.valid.js +23 -0
  22. package/test/fixtures/inherit.valid.js +52 -0
  23. package/test/fixtures/intellisense.valid.js +37 -0
  24. package/test/fixtures/json.invalid.js +22 -0
  25. package/test/fixtures/json.valid.js +103 -0
  26. package/test/fixtures/misc.invalid.js +25 -0
  27. package/test/fixtures/misc.valid.js +33 -0
  28. package/test/fixtures/numeric.invalid.js +38 -0
  29. package/test/fixtures/numeric.valid.js +63 -0
  30. package/test/fixtures/playcanvas.d.ts +47963 -0
  31. package/test/fixtures/program.invalid.js +11 -0
  32. package/test/fixtures/text.valid.js +30 -0
  33. package/test/fixtures/vector.invalid.js +13 -0
  34. package/test/fixtures/vector.valid.js +23 -0
  35. package/test/tests/invalid/asset.test.js +48 -0
  36. package/test/tests/invalid/curve.test.js +24 -0
  37. package/test/tests/invalid/json.test.js +28 -0
  38. package/test/tests/invalid/misc.test.js +42 -0
  39. package/test/tests/invalid/numeric.test.js +56 -0
  40. package/test/tests/invalid/program.test.js +18 -0
  41. package/test/tests/invalid/vector.test.js +29 -0
  42. package/test/tests/valid/asset.test.js +51 -0
  43. package/test/tests/valid/checkbox.test.js +50 -0
  44. package/test/tests/valid/color.test.js +50 -0
  45. package/test/tests/valid/curve.test.js +77 -0
  46. package/test/tests/valid/entity.test.js +50 -0
  47. package/test/tests/valid/enum.test.js +74 -0
  48. package/test/tests/valid/export.test.js +56 -0
  49. package/test/tests/valid/inherit.test.js +103 -0
  50. package/test/tests/valid/intellisense.test.js +36 -0
  51. package/test/tests/valid/json.test.js +150 -0
  52. package/test/tests/valid/misc.test.js +47 -0
  53. package/test/tests/valid/numeric.test.js +104 -0
  54. package/test/tests/valid/text.test.js +59 -0
  55. package/test/tests/valid/vector.test.js +54 -0
  56. package/test/utils.js +53 -0
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ unit-test-node:
11
+ name: Unit Test
12
+ runs-on: ubuntu-latest
13
+ timeout-minutes: 10
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Node.js 18.x
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 18.x
22
+ cache: 'npm'
23
+
24
+ - name: Install dependencies
25
+ run: npm clean-install --progress=false --no-fund
26
+
27
+ - name: Run unit tests
28
+ run: npm test
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # PlayCanvas Attribute Parser
2
+
3
+ This the JSDoc attribute parser used in the PlayCanvas editor. It's used to collect metadata from a script via it's jsdoc tags.
4
+
5
+ It takes a script like this...
6
+
7
+ ```javascript
8
+ class Rotator extends ScriptType {
9
+ /**
10
+ * @attribute
11
+ * Speed determines how fast to rotate things"
12
+ */
13
+ speed = new Vec3()
14
+
15
+ /**
16
+ * @attribute
17
+ * An array of Entities to rotate
18
+ *
19
+ * @type {Entity[]}
20
+ */
21
+ thingsToRotate
22
+ }
23
+ ```
24
+
25
+ and turns it into this...
26
+
27
+
28
+ ```json
29
+ {
30
+ "rotator": {
31
+ "attributes": {
32
+ "speed": {
33
+ "type": "vec3",
34
+ "name": "speed",
35
+ "array": false,
36
+ "description": "Speed determines how fast to rotate things\"",
37
+ "default": [
38
+ 0,
39
+ 0,
40
+ 0
41
+ ]
42
+ },
43
+ "thingsToRotate": {
44
+ "type": "entity",
45
+ "name": "thingsToRotate",
46
+ "array": true,
47
+ "description": "An array of Entities to rotate",
48
+ "default": null
49
+ }
50
+ },
51
+ "errors": []
52
+ }
53
+ }
54
+ ```
55
+
56
+ It takes the JSDocs and values and outputs the metadata in a serializable format.
57
+
58
+ ### Usage
59
+
60
+ ```javascript
61
+ // Initialise the parser
62
+ const parser = new JSDocParser();
63
+ await parser.init();
64
+
65
+ // fetch your program source {[filename: string, contents: string][]}
66
+ const scripts = await fetchScripts([...paths, './playcanvas.d.ts']);
67
+
68
+ // Parse the program, starting from the first path
69
+ const [attribute, errors] parser.parseAttributes(paths[0], scripts);
70
+ ```
71
+
72
+ ### Attribute structure
73
+
74
+ You can learn more about the special tags [available here ](https://github.com/playcanvas/attribute-parser/tree/main/test/fixtures)
75
+
76
+
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@playcanvas/attribute-parser",
3
+ "type": "module",
4
+ "main": "src/index.js",
5
+ "engines": {
6
+ "node": ">=18.0.0"
7
+ },
8
+ "version": "1.0.0",
9
+ "dependencies": {
10
+ "@microsoft/tsdoc": "^0.15.0",
11
+ "@typescript/vfs": "^1.6.0",
12
+ "typescript": "^5.5.3"
13
+ },
14
+ "devDependencies": {
15
+ "chai": "^5.1.1",
16
+ "mocha": "^10.6.0"
17
+ },
18
+ "scripts": {
19
+ "test": "mocha test/**/*.test.js --timeout 10000"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }
package/src/index.js ADDED
@@ -0,0 +1,226 @@
1
+ import ts from 'typescript';
2
+ import { createSystem, createDefaultMapFromNodeModules, createVirtualTypeScriptEnvironment } from '@typescript/vfs';
3
+
4
+ import { ScriptParser } from './parsers/script-parser.js';
5
+ import { createDefaultMapFromCDN, flatMapAnyNodes, getExportedNodes, getType, inheritsFrom, isAliasedClassDeclaration } from './utils/ts-utils.js';
6
+
7
+ const toLowerCamelCase = str => str[0].toLowerCase() + str.substring(1);
8
+
9
+ const COMPILER_OPTIONS = {
10
+ strictPropertyInitialization: false, // Allow uninitialized properties
11
+ target: ts.ScriptTarget.ES2022, // If this version changes, the types must be updated in the /rollup.config.mjs
12
+ module: ts.ModuleKind.CommonJS,
13
+ checkJs: true, // Enable JSDoc parsing
14
+ allowJs: true,
15
+ noImplicitAny: false, // Allow implicit any
16
+ baseUrl: './',
17
+ paths: {
18
+ "playcanvas": ["/playcanvas.js"]
19
+ }
20
+ };
21
+
22
+ export class JSDocParser {
23
+ /**
24
+ * @type {import('@typescript/vfs').VirtualTypeScriptEnvironment}
25
+ * @private
26
+ */
27
+ _env;
28
+
29
+ /**
30
+ * An internal array of program errors
31
+ * @private
32
+ */
33
+ _errors = [];
34
+
35
+ async init(basePath = '') {
36
+ if (this._env) {
37
+ return;
38
+ }
39
+
40
+ let fsMap;
41
+ if (basePath) {
42
+ fsMap = await createDefaultMapFromCDN({ target: ts.ScriptTarget.ES2022 }, basePath, ts);
43
+ } else {
44
+ fsMap = await createDefaultMapFromNodeModules(COMPILER_OPTIONS, ts);
45
+ }
46
+
47
+ // Set up the virtual file system and environment
48
+ const system = createSystem(fsMap);
49
+ this._env = createVirtualTypeScriptEnvironment(system, Array.from(fsMap.keys()), ts, COMPILER_OPTIONS);
50
+ }
51
+
52
+ /**
53
+ * Creates program from fileName and scriptsContents
54
+ *
55
+ * @param {[string, string][]} newFiles - An array of {[path]: [content]} which represent the files in the program
56
+ * @param {string[]} removedFiles - An array of file paths that should be removed from the program
57
+ * @returns {{ parser: ScriptParser, program: ts.Program,
58
+ * errors: import('./parsers/script-parser.js').ValidationError[]}} - The extracted attributes
59
+ */
60
+ updateProgram(newFiles = [], removedFiles = []) {
61
+ const errors = [];
62
+
63
+ // Add custom files to the map
64
+ newFiles.forEach(([filePath, fileContent]) => {
65
+ if (this._env.getSourceFile(filePath)) {
66
+ this._env.updateFile(filePath, fileContent);
67
+ } else {
68
+ this._env.createFile(filePath, fileContent);
69
+ }
70
+ });
71
+
72
+ // Remove deleted Files
73
+ removedFiles.forEach((filePath) => {
74
+ this._env.deleteFile(filePath);
75
+ });
76
+
77
+
78
+ this.parser = new ScriptParser(this._env);
79
+ this.program = this._env.languageService.getProgram();
80
+
81
+ // Check for compiler errors
82
+ this.parser.validateProgram(errors);
83
+
84
+ this._errors = errors;
85
+
86
+ return errors;
87
+ }
88
+
89
+ /**
90
+ * Returns all the valid ESM Scripts within a file
91
+ * @param fileName - The file name in the program to check
92
+ * @returns {import('typescript').Node[]} - An array of any exported ESM Script nodes within the file
93
+ */
94
+ getAllEsmScripts(fileName) {
95
+
96
+ const typeChecker = this.program.getTypeChecker();
97
+
98
+ // Find the Script class in the PlayCanvas namespace
99
+ const pcTypes = this.program.getSourceFile('/playcanvas.d.ts');
100
+
101
+ if (!pcTypes) {
102
+ throw new Error(`PlayCanvas Types must be supplied`);
103
+ }
104
+
105
+ // Parse the source file and pc types
106
+ const sourceFile = this.program.getSourceFile(fileName);
107
+
108
+ if (!sourceFile) {
109
+ throw new Error(`Source file ${fileName} not found`);
110
+ }
111
+
112
+ // Extract all exported nodes
113
+ const nodes = getExportedNodes(this.program, sourceFile);
114
+
115
+ const esmScriptClass = pcTypes.statements.find(node => node.kind === ts.SyntaxKind.ClassDeclaration && node.name.text === 'Script')?.symbol;
116
+
117
+ // Check if the file exports a class that inherits from `Script`
118
+ return Array.from(nodes).filter(node => isAliasedClassDeclaration(node, typeChecker) && inheritsFrom(node, typeChecker, esmScriptClass));
119
+
120
+ }
121
+
122
+ /**
123
+ * Analyzes the specified TypeScript file and extracts metadata from JSDoc comments.
124
+ *
125
+ * @param {string} fileName - The name of the file in the program to run
126
+ * @returns {[results: object, errors: import('./parsers/script-parser.js').ValidationError[]]} - The extracted attributes
127
+ */
128
+ parseAttributes(fileName) {
129
+ const results = {};
130
+ const errors = [...this._errors];
131
+
132
+ if(errors.length > 0 ){
133
+ return [results, errors];
134
+ }
135
+
136
+ // Extract all exported nodes
137
+ const nodes = this.getAllEsmScripts(fileName);
138
+
139
+ // Extract attributes from each script
140
+ nodes.forEach((node) => {
141
+ const name = toLowerCamelCase(node.name.text);
142
+ const opts = results[name] = { attributes: {}, errors: [] };
143
+ this.parser.extractAttributes(node, opts);
144
+ });
145
+
146
+ return [results, errors];
147
+ }
148
+
149
+ /**
150
+ * Analyzes the specified TypeScript file and extracts metadata from JSDoc comments.
151
+ * @param {string} fileName - The name of the file to run
152
+ * @returns {[results: object, errors: import('./parsers/script-parser.js').ValidationError[]]} - The extracted attributes
153
+ */
154
+ getAttributes(fileName) {
155
+ const results = {};
156
+ const errors = [...this._errors];
157
+
158
+ if(errors.length > 0 ){
159
+ return [results, errors];
160
+ }
161
+
162
+ const typeChecker = this.program.getTypeChecker();
163
+
164
+ // Find the Script class in the PlayCanvas namespace
165
+ const pcTypes = this.program.getSourceFile('/playcanvas.d.ts');
166
+ const esmScriptClass = pcTypes.statements.find(node => node.kind === ts.SyntaxKind.ClassDeclaration && node.name.text === 'Script')?.symbol;
167
+
168
+ // Parse the source file and pc types
169
+ const sourceFile = this.program.getSourceFile(fileName);
170
+
171
+ if (!sourceFile) {
172
+ throw new Error(`Source file ${fileName} not found`);
173
+ }
174
+
175
+ // Filter out classes that do not inherit from `Script` or are interfaces
176
+ const isInterface = node => node.jsDoc.some(jsdoc => jsdoc.tags?.some(tag => tag.tagName.getText() === 'interface'));
177
+
178
+ const nodes = flatMapAnyNodes(sourceFile, (node) => {
179
+ if (!ts.isClassDeclaration(node)) {
180
+ return false;
181
+ }
182
+
183
+ return inheritsFrom(node, typeChecker, esmScriptClass) || isInterface(node);
184
+ });
185
+
186
+ for (const node of nodes) {
187
+ const name = toLowerCamelCase(node.name.text);
188
+ const members = [];
189
+
190
+ for (const member of node.members) {
191
+ if (member.kind !== ts.SyntaxKind.PropertyDeclaration) {
192
+ continue;
193
+ }
194
+
195
+ // Extract JSDoc tags
196
+ const tags = member.jsDoc.map(jsdoc => jsdoc.tags?.map(tag => tag.tagName.getText()) ?? []).flat();
197
+
198
+ const name = member.name.getText();
199
+ const type = getType(member, typeChecker);
200
+ if (!type) {
201
+ continue;
202
+ }
203
+
204
+ const namePos = ts.getLineAndCharacterOfPosition(member.getSourceFile(), member.name.getStart());
205
+
206
+ const jsdocNode = member.jsDoc[member.jsDoc.length - 1];
207
+ const jsdocPos = jsdocNode ? ts.getLineAndCharacterOfPosition(member.getSourceFile(), jsdocNode.getStart()) : null;
208
+
209
+ const data = {
210
+ name,
211
+ type: type.name + (type.array ? '[]' : ''),
212
+ isAttribute: tags.includes('attribute'),
213
+ start: jsdocNode ? { line: jsdocPos.line + 1, column: jsdocPos.character + 1 } :
214
+ { line: namePos.line + 1, column: namePos.character + 1 },
215
+ end: { line: namePos.line + 1, column: namePos.character + name.length + 1 }
216
+ };
217
+
218
+ members.push(data);
219
+ }
220
+
221
+ results[name] = members;
222
+ }
223
+
224
+ return [results, errors];
225
+ }
226
+ }
@@ -0,0 +1,280 @@
1
+ import ts from 'typescript';
2
+ import { TSDocConfiguration, TSDocTagDefinition, TSDocTagSyntaxKind, TextRange, TSDocParser } from "@microsoft/tsdoc";
3
+ import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType } from '../utils/ts-utils.js';
4
+
5
+ import { ParsingError } from "./parsing-error.js";
6
+ import { hasTag } from "../utils/attribute-utils.js";
7
+ import { parseTag, validateTag } from "../utils/tag-utils.js";
8
+
9
+ /**
10
+ * A class to parse JSDoc comments and extract attribute metadata.
11
+ */
12
+ export class AttributeParser {
13
+ /**
14
+ * Creates a new instance of the ScriptParser class.
15
+ *
16
+ * @param {Map<string, string>} tags - An set of tag definitions and their validators to add to the parser
17
+ * @param {object} env - The TypeScript environment to use
18
+ * @param {Map<string, Function>} typeSerializerMap - A map of custom serializers
19
+ */
20
+ constructor(tags = new Map(), env, typeSerializerMap) {
21
+
22
+ this.env = env;
23
+ this.tagTypeAnnotations = tags;
24
+ this.typeSerializerMap = typeSerializerMap;
25
+ this.program = env.languageService.getProgram();
26
+ this.typeChecker = this.program.getTypeChecker();
27
+
28
+ this.attributeTag = new TSDocTagDefinition({
29
+ tagName: '@attribute', syntaxKind: TSDocTagSyntaxKind.ModifierTag
30
+ });
31
+
32
+ this.typeTag = new TSDocTagDefinition({
33
+ tagName: '@type', syntaxKind: TSDocTagSyntaxKind.BlockTag
34
+ });
35
+
36
+ const conf = new TSDocConfiguration();
37
+ conf.validation.reportUnsupportedTags = false;
38
+
39
+ conf.addTagDefinition(this.attributeTag);
40
+
41
+ tags.forEach((_, tag) => {
42
+ const tagDefinition = new TSDocTagDefinition({
43
+ tagName: `@${tag}`, syntaxKind: TSDocTagSyntaxKind.BlockTag, allowMultiple: false
44
+ });
45
+ conf.addTagDefinition(tagDefinition);
46
+ });
47
+
48
+ this.parser = new TSDocParser(conf);
49
+ }
50
+
51
+ /**
52
+ * Parses a JSdoc comment and extracts any associated metadata if the `@attribute`
53
+ * modifier is used. Returns null if this is not an attribute comment.
54
+ *
55
+ * @param {TextRange} comment - The comment to parse
56
+ * @param {ts.Node} node - The node to parse
57
+ * @param {ParsingError[]} errors - An array to store any parsing errors
58
+ * @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
59
+ * @returns {*} - The extracted metadata or null if no metadata was found
60
+ */
61
+ parseAttributeComment(comment, node, errors = [], requiresAttributeTag = true) {
62
+
63
+ const parserContext = this.parser.parseRange(comment);
64
+ const docComment = parserContext.docComment;
65
+ const file = node.getSourceFile();
66
+
67
+ // Check if the comment has an attribute tag
68
+ if (!requiresAttributeTag || hasTag('attribute', node)) {
69
+
70
+ // Fetch the primary attribute metadata
71
+ const attribute = this.getNodeAsAttribute(node, errors);
72
+
73
+ if (!attribute) return;
74
+
75
+ // Extract the description from the summary section
76
+ const description = extractTextFromDocNode(docComment.summarySection);
77
+ if (description) {
78
+ attribute.description = description.split('.')[0];
79
+ }
80
+
81
+ // Extract metadata from custom tags
82
+ const jsDocTags = node.jsDoc?.[0]?.tags ?? [];
83
+ jsDocTags.forEach((tag) => {
84
+ let commentText = tag.comment;
85
+
86
+ // Check if the tag is a supported tag
87
+ if (this.tagTypeAnnotations.has(tag.tagName.text)) {
88
+ try {
89
+ const value = parseTag(commentText);
90
+ const tagTypeAnnotation = this.tagTypeAnnotations.get(tag.tagName.text);
91
+
92
+ // Tags like @resource with string values do not need quotations, so we need to manually add them
93
+ if (typeof value === 'string') commentText = `"${commentText}"`;
94
+
95
+ validateTag(commentText, tagTypeAnnotation, this.env);
96
+ attribute[tag.tagName.text] = value;
97
+
98
+ } catch (error) {
99
+ const { line, character } = file.getLineAndCharacterOfPosition(tag.getStart());
100
+ const parseError = new ParsingError(tag, `Error (${line}, ${character}): Parsing Tag '@${tag.tagName.text} ${commentText}' - ${error.message}`);
101
+ errors.push(parseError);
102
+ }
103
+ }
104
+ });
105
+
106
+ return attribute;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Returns the leading block comments for an typescript AST node.
112
+ *
113
+ * @param {import('typescript').Node} node - The node to parse
114
+ * @param {string[]} errors - An array to store any parsing errors
115
+ * @returns {import('@microsoft/tsdoc').DocComment} - The extracted doc comment;
116
+ */
117
+ getCommentsForNode(node, errors = []) {
118
+ const fullText = node.getSourceFile().getFullText();
119
+ const comments = getLeadingBlockCommentRanges(node, fullText);
120
+
121
+ if (comments.length === 0) return null;
122
+
123
+ const lastComment = comments[0];
124
+ const textRange = TextRange.fromStringRange(fullText, lastComment.pos, lastComment.end);
125
+
126
+ const parserContext = this.parser.parseRange(textRange);
127
+ const docComment = parserContext.docComment;
128
+
129
+ // Check for comment parsing errors and flag them
130
+ this.populateErrors(node, parserContext, errors);
131
+
132
+ return docComment;
133
+ }
134
+
135
+ /**
136
+ * Populates the errors array with any parsing errors found in the parser context.
137
+ *
138
+ * @param {import('typescript').Node} node - The node to parse
139
+ * @param {import('@microsoft/tsdoc').ParserContext} parserContext - The parser context to check for errors
140
+ * @param {string[]} errors - An array to store any parsing errors
141
+ */
142
+ populateErrors(node, parserContext, errors) {
143
+ if (parserContext.log.messages.length > 0) {
144
+ const sourceFile = node.getSourceFile();
145
+
146
+ const formattedErrors = parserContext.log.messages.map((message) => {
147
+ const location = sourceFile.getLineAndCharacterOfPosition(message.textRange.pos);
148
+ return `${sourceFile.fileName}(${location.line + 1},${location.character + 1}): [TSDoc] ${message}`;
149
+ });
150
+
151
+ errors.push(...formattedErrors);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Extract the public members and their values from a TypeScript node.
157
+ *
158
+ * @param {ts.Node} node - The node to parse
159
+ * @param {ParsingError[]} errors - An array to store any parsing errors
160
+ * @returns {*} - The extracted members
161
+ */
162
+ getNodeAsAttribute(node, errors = []) {
163
+
164
+ const name = node.name && ts.isIdentifier(node.name) && node.name.text;
165
+ const { type, name: typeName, array } = getType(node, this.typeChecker);
166
+ const enums = this.getEnumMembers(node, errors);
167
+ let value = null;
168
+
169
+ // we don't need to serialize the value for arrays
170
+ const serializer = !array && this.typeSerializerMap.get(type);
171
+ if (serializer) {
172
+ try {
173
+ value = serializer(node.initializer ?? node);
174
+ } catch (error) {
175
+ errors.push(error);
176
+ return;
177
+ }
178
+ }
179
+
180
+ return { type, name, typeName, array, value, enum: enums };
181
+ }
182
+
183
+ /**
184
+ * Extracts the members of an enum from a TypeScript node.
185
+ *
186
+ * @param {import('typescript').Node} node - The TypeScript node to analyze
187
+ * @param {ParsingError[]} errors - An array to store any parsing errors
188
+ * @returns {Object<string, *>[]} - An array of enum members
189
+ */
190
+ getEnumMembers(node, errors) {
191
+ let typeNode = null;
192
+ let members = [];
193
+
194
+ // Check if there's a type annotation directly on the variable declaration
195
+ if (ts.isVariableDeclaration(node) && node.type) {
196
+ typeNode = node.type;
197
+ } else {
198
+ // Check for JSDoc annotations
199
+ const jsDocs = ts.getJSDocTags(node);
200
+ const typeTag = jsDocs.find(doc => doc.tagName.text === "type");
201
+ if (typeTag && ts.isJSDocTypeTag(typeTag)) {
202
+ typeNode = typeTag.typeExpression;
203
+ }
204
+ }
205
+
206
+ if (typeNode && ts.isTypeReferenceNode(typeNode.type)) {
207
+
208
+ // resolve the symbol of the type
209
+ let symbol = this.typeChecker.getSymbolAtLocation(typeNode.type.typeName);
210
+
211
+ // Resolve aliases, which are common with imports
212
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
213
+ symbol = this.typeChecker.getAliasedSymbol(symbol);
214
+ }
215
+
216
+ if (symbol) {
217
+ const declarations = symbol.getDeclarations();
218
+
219
+ if (declarations && declarations.length > 0) {
220
+
221
+ declarations.forEach((declaration) => {
222
+
223
+ // Check if the declaration is a TypeScript enum
224
+ if (ts.isEnumDeclaration(declaration)) {
225
+ members = declaration.members.map(member => member.name.getText());
226
+ }
227
+
228
+ // Additionally check for JSDoc enum tag
229
+ const jsDocs = ts.getJSDocTags(declaration);
230
+ const enumTag = jsDocs.find(doc => doc.tagName.text === "enum");
231
+ if (enumTag && ts.isJSDocEnumTag(enumTag)) {
232
+ // This assumes enum members are correctly annotated in JSDoc comments
233
+ // Example: @enum {type} EnumName { Member1, Member2 }
234
+ members = members.concat(this.getMembers(declaration.initializer, errors)); // declaration.initializer.properties.map(member => member.name.getText());
235
+ }
236
+ });
237
+ }
238
+ }
239
+ }
240
+
241
+ return members;
242
+ }
243
+
244
+ /**
245
+ * Extract the public members and their values from a TypeScript node.
246
+ *
247
+ * @param {ts.Node} node - The node to parse
248
+ * @param {ParsingError[]} errors - An array to store any parsing errors
249
+ * @returns {any[]} - The extracted members
250
+ */
251
+ getMembers(node, errors = []) {
252
+
253
+ const members = [];
254
+
255
+ if (ts.isObjectLiteralExpression(node)) {
256
+ node.properties.forEach((property) => {
257
+
258
+ if (ts.isPropertyAssignment(property)) {
259
+ const name = property.name && ts.isIdentifier(property.name) && property.name.text;
260
+ let value;
261
+
262
+ const node = property.initializer;
263
+
264
+ // Enums can only contain primitives (string|number|boolean)
265
+ if (ts.isNumericLiteral(node)) {
266
+ value = parseFloat(node.getText());
267
+ } else if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
268
+ value = node.kind === ts.SyntaxKind.TrueKeyword;
269
+ } else {
270
+ value = node.getText();
271
+ }
272
+
273
+ members.push({ [name]: value });
274
+ }
275
+ });
276
+ }
277
+
278
+ return members;
279
+ }
280
+ }
@@ -0,0 +1,14 @@
1
+ export class ParsingError {
2
+ constructor(node, message) {
3
+ this.node = node; // AST node which caused the error
4
+ this.message = message; // Description of the error
5
+ }
6
+
7
+ toString() {
8
+ // Ensure node.location exists and has the necessary properties
9
+ const file = this.node.getSourceFile();
10
+ const fileName = file.fileName;
11
+ const start = this.node.getSourceFile().getLineAndCharacterOfPosition(this.node.getStart());
12
+ return `ParsingError: ${this.message} at ${fileName}:${start.line + 1}:${start.character}`;
13
+ }
14
+ }