@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.
Files changed (77) hide show
  1. package/LICENSE.md +19 -0
  2. package/dist/index.js +194566 -0
  3. package/dist/libs.d.ts +50978 -0
  4. package/package.json +49 -9
  5. package/types/index.d.ts +55 -0
  6. package/types/parsers/attribute-parser.d.ts +55 -0
  7. package/types/parsers/parsing-error.d.ts +6 -0
  8. package/types/parsers/script-parser.d.ts +124 -0
  9. package/types/utils/attribute-utils.d.ts +5 -0
  10. package/{src/utils/generic-utils.js → types/utils/generic-utils.d.ts} +1 -10
  11. package/types/utils/tag-utils.d.ts +16 -0
  12. package/types/utils/ts-utils.d.ts +103 -0
  13. package/.github/workflows/ci.yml +0 -49
  14. package/eslint.config.mjs +0 -35
  15. package/renovate.json +0 -25
  16. package/src/parsers/attribute-parser.js +0 -197
  17. package/src/parsers/parsing-error.js +0 -14
  18. package/src/parsers/script-parser.js +0 -432
  19. package/src/utils/attribute-utils.js +0 -53
  20. package/src/utils/tag-utils.js +0 -73
  21. package/src/utils/ts-utils.js +0 -634
  22. package/test/fixtures/asset.invalid.js +0 -27
  23. package/test/fixtures/asset.invalid.ts +0 -24
  24. package/test/fixtures/asset.valid.js +0 -26
  25. package/test/fixtures/asset.valid.ts +0 -21
  26. package/test/fixtures/checkbox.valid.js +0 -23
  27. package/test/fixtures/color.valid.js +0 -23
  28. package/test/fixtures/conditional.valid.js +0 -22
  29. package/test/fixtures/curve.invalid.js +0 -29
  30. package/test/fixtures/curve.valid.js +0 -49
  31. package/test/fixtures/empty.valid.js +0 -0
  32. package/test/fixtures/entity.valid.js +0 -23
  33. package/test/fixtures/enum.valid.js +0 -66
  34. package/test/fixtures/enum.valid.ts +0 -50
  35. package/test/fixtures/example.dep.js +0 -9
  36. package/test/fixtures/export.import.js +0 -8
  37. package/test/fixtures/export.valid.js +0 -26
  38. package/test/fixtures/inherit.valid.js +0 -45
  39. package/test/fixtures/intellisense.valid.js +0 -40
  40. package/test/fixtures/json.invalid.js +0 -23
  41. package/test/fixtures/json.valid.js +0 -115
  42. package/test/fixtures/json.valid.ts +0 -81
  43. package/test/fixtures/misc.invalid.js +0 -24
  44. package/test/fixtures/misc.valid.js +0 -51
  45. package/test/fixtures/numeric.invalid.js +0 -38
  46. package/test/fixtures/numeric.valid.js +0 -63
  47. package/test/fixtures/playcanvas.d.ts +0 -47963
  48. package/test/fixtures/program.invalid.js +0 -12
  49. package/test/fixtures/program.valid.js +0 -27
  50. package/test/fixtures/text.valid.js +0 -31
  51. package/test/fixtures/vector.invalid.js +0 -14
  52. package/test/fixtures/vector.valid.js +0 -24
  53. package/test/tests/invalid/asset.test.js +0 -47
  54. package/test/tests/invalid/curve.test.js +0 -23
  55. package/test/tests/invalid/json.test.js +0 -27
  56. package/test/tests/invalid/misc.test.js +0 -41
  57. package/test/tests/invalid/numeric.test.js +0 -55
  58. package/test/tests/invalid/program.test.js +0 -17
  59. package/test/tests/invalid/vector.test.js +0 -28
  60. package/test/tests/valid/asset.test.js +0 -59
  61. package/test/tests/valid/checkbox.test.js +0 -57
  62. package/test/tests/valid/color.test.js +0 -49
  63. package/test/tests/valid/conditional.test.js +0 -45
  64. package/test/tests/valid/curve.test.js +0 -76
  65. package/test/tests/valid/empty.test.js +0 -17
  66. package/test/tests/valid/entity.test.js +0 -49
  67. package/test/tests/valid/enum.test.js +0 -79
  68. package/test/tests/valid/export.test.js +0 -67
  69. package/test/tests/valid/inherit.test.js +0 -110
  70. package/test/tests/valid/intellisense.test.js +0 -39
  71. package/test/tests/valid/json.test.js +0 -315
  72. package/test/tests/valid/misc.test.js +0 -54
  73. package/test/tests/valid/numeric.test.js +0 -103
  74. package/test/tests/valid/program.test.js +0 -23
  75. package/test/tests/valid/text.test.js +0 -58
  76. package/test/tests/valid/vector.test.js +0 -53
  77. package/test/utils.js +0 -54
@@ -1,197 +0,0 @@
1
- import * as ts from 'typescript';
2
-
3
- import { ParsingError } from './parsing-error.js';
4
- import { hasTag } from '../utils/attribute-utils.js';
5
- import { parseTag, validateTag } from '../utils/tag-utils.js';
6
- import { getLiteralValue, getType } from '../utils/ts-utils.js';
7
-
8
- /**
9
- * A class to parse JSDoc comments and extract attribute metadata.
10
- */
11
- export class AttributeParser {
12
- /**
13
- * Creates a new instance of the ScriptParser class.
14
- *
15
- * @param {Map<string, string>} tags - An set of tag definitions and their validators to add to the parser
16
- * @param {object} env - The TypeScript environment to use
17
- * @param {Map<string, Function>} typeSerializerMap - A map of custom serializers
18
- */
19
- constructor(tags = new Map(), env, typeSerializerMap) {
20
-
21
- this.env = env;
22
- this.tagTypeAnnotations = tags;
23
- this.typeSerializerMap = typeSerializerMap;
24
- this.program = env.languageService.getProgram();
25
- this.typeChecker = this.program.getTypeChecker();
26
-
27
- }
28
-
29
- /**
30
- * Parses a JSdoc comment and extracts any associated metadata if the `@attribute`
31
- * modifier is used. Returns null if this is not an attribute comment.
32
- *
33
- * @param {ts.Node} node - The node to parse
34
- * @param {ParsingError[]} errors - An array to store any parsing errors
35
- * @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
36
- * @returns {*} - The extracted metadata or null if no metadata was found
37
- */
38
- parseAttributeComment(node, errors = [], requiresAttributeTag = true) {
39
-
40
- // Check if the comment has an attribute tag
41
- if (!requiresAttributeTag || hasTag('attribute', node)) {
42
-
43
- // Fetch the primary attribute metadata
44
- const attribute = this.getNodeAsAttribute(node, errors);
45
-
46
- if (!attribute) return;
47
-
48
- // Extract the description from the summary section
49
- attribute.description = node.jsDoc?.[0]?.comment || '';
50
-
51
- // Extract metadata from custom tags
52
- const jsDocTags = node.jsDoc?.[0]?.tags ?? [];
53
- jsDocTags.forEach((tag) => {
54
- let commentText = tag.comment;
55
-
56
- // Check if the tag is a supported tag
57
- if (this.tagTypeAnnotations.has(tag.tagName.text)) {
58
- try {
59
- const value = parseTag(commentText);
60
- const tagTypeAnnotation = this.tagTypeAnnotations.get(tag.tagName.text);
61
-
62
- // Tags like @resource with string values do not need quotations, so we need to manually add them
63
- if (typeof value === 'string') commentText = `"${commentText}"`;
64
-
65
- validateTag(commentText, tagTypeAnnotation, this.env);
66
- attribute[tag.tagName.text] = value;
67
-
68
- } catch (error) {
69
- const file = node.getSourceFile();
70
- const { line, character } = file.getLineAndCharacterOfPosition(tag.getStart());
71
- const parseError = new ParsingError(tag, `Error (${line}, ${character}): Parsing Tag '@${tag.tagName.text} ${commentText}' - ${error.message}`);
72
- errors.push(parseError);
73
- }
74
- }
75
- });
76
-
77
- return attribute;
78
- }
79
- }
80
-
81
- /**
82
- * Extract the public members and their values from a TypeScript node.
83
- *
84
- * @param {ts.Node} node - The node to parse
85
- * @param {ParsingError[]} errors - An array to store any parsing errors
86
- * @returns {*} - The extracted members
87
- */
88
- getNodeAsAttribute(node, errors = []) {
89
-
90
- const name = node.name && ts.isIdentifier(node.name) && node.name.text;
91
- const { type, name: typeName, array } = getType(node, this.typeChecker);
92
- const enums = this.getEnumMembers(node, errors);
93
- let value = null;
94
-
95
- // we don't need to serialize the value for arrays
96
- const serializer = !array && this.typeSerializerMap.get(typeName);
97
- if (serializer) {
98
- try {
99
- value = serializer(node.initializer ?? node, this.typeChecker);
100
- } catch (error) {
101
- errors.push(error);
102
- return;
103
- }
104
- }
105
-
106
- return { type, name, typeName, array, value, enum: enums };
107
- }
108
-
109
- /**
110
- * Extracts the members of an enum from a TypeScript node.
111
- *
112
- * @param {import('typescript').Node} node - The TypeScript node to analyze
113
- * @param {ParsingError[]} errors - An array to store any parsing errors
114
- * @returns {Object<string, *>[]} - An array of enum members
115
- */
116
- getEnumMembers(node, errors) {
117
- let typeNode = null;
118
- let members = [];
119
-
120
- // Check if there's a type annotation directly on the variable declaration
121
- if (node.type) {
122
- typeNode = node;
123
- } else {
124
- // Check for JSDoc annotations
125
- const jsDocs = ts.getJSDocTags(node);
126
- const typeTag = jsDocs.find(doc => doc.tagName.text === 'type');
127
- if (typeTag && ts.isJSDocTypeTag(typeTag)) {
128
- typeNode = typeTag.typeExpression;
129
- }
130
- }
131
-
132
- // Also consider the elementType
133
- const type = typeNode && (typeNode.type.elementType ?? typeNode.type);
134
-
135
- if (typeNode && ts.isTypeReferenceNode(type)) {
136
-
137
- // resolve the symbol of the type
138
- let symbol = this.typeChecker.getSymbolAtLocation(type.typeName);
139
-
140
- // Resolve aliases, which are common with imports
141
- if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
142
- symbol = this.typeChecker.getAliasedSymbol(symbol);
143
- }
144
-
145
- if (symbol) {
146
- const declarations = symbol.getDeclarations();
147
-
148
- if (declarations && declarations.length > 0) {
149
-
150
- declarations.forEach((declaration) => {
151
-
152
- // Check if the declaration is a TypeScript enum
153
- if (ts.isEnumDeclaration(declaration)) {
154
- members = declaration.members.map(member => ({ [member.name.text]: member.initializer.text }));
155
- }
156
-
157
- // Additionally check for JSDoc enum tag
158
- const jsDocs = ts.getJSDocTags(declaration);
159
- const enumTag = jsDocs.find(doc => doc.tagName.text === 'enum');
160
- if (enumTag && ts.isJSDocEnumTag(enumTag)) {
161
- // This assumes enum members are correctly annotated in JSDoc comments
162
- // Example: @enum {type} EnumName { Member1, Member2 }
163
- members = members.concat(this.getMembers(declaration.initializer, errors)); // declaration.initializer.properties.map(member => member.name.getText());
164
- }
165
- });
166
- }
167
- }
168
- }
169
-
170
- return members;
171
- }
172
-
173
- /**
174
- * Extract the public members and their values from a TypeScript node.
175
- *
176
- * @param {ts.Node} node - The node to parse
177
- * @returns {any[]} - The extracted members
178
- */
179
- getMembers(node) {
180
-
181
- const members = [];
182
-
183
- if (ts.isObjectLiteralExpression(node)) {
184
- node.properties.forEach((property) => {
185
-
186
- if (ts.isPropertyAssignment(property)) {
187
- const isValidName = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name);
188
- const name = isValidName && property.name.text;
189
- const value = getLiteralValue(property.initializer, this.typeChecker);
190
- members.push({ [name]: value });
191
- }
192
- });
193
- }
194
-
195
- return members;
196
- }
197
- }
@@ -1,14 +0,0 @@
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
- }
@@ -1,432 +0,0 @@
1
- import * as ts from 'typescript';
2
-
3
- import { AttributeParser } from './attribute-parser.js';
4
- import { ParsingError } from './parsing-error.js';
5
- import { hasTag } from '../utils/attribute-utils.js';
6
- import { zipArrays } from '../utils/generic-utils.js';
7
- import { flatMapAnyNodes, getJSDocTags, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
8
-
9
- /**
10
- * @typedef {object} Attribute
11
- * @property {string} type - The type of the attribute
12
- * @property {boolean} array - Whether the attribute is an array type of not
13
- * @property {string} [description] - The description of the attribute
14
- * @property {string} [placeholder] - Placeholder for Editor's for field UI.
15
- * @property {*} [defaultValue] - The default value of the attribute
16
- * @property {Object<string, number|boolean|string>[]} [enum] - An array of possible values for the attribute
17
- * @property {{ min: number, max: number}} [range] - Numerical constraints for the attribute
18
- * @property {number} [precision] - Numerical constraints for the attribute
19
- * @property {number} [step] - Numerical constraints for the attribute
20
- * @property {string} [resource] - Name of asset type to be used in 'asset' type attribute
21
- */
22
-
23
- /**
24
- * @typedef {object} ValidationError
25
- * @property {string} message - The error message
26
- * @property {string} [file] - The file the error occurred in
27
- * @property {number} [line] - The line the error occurred on
28
- * @property {number} [column] - The column the error occurred on
29
- * @property {number} [length] - The length of the error
30
- */
31
-
32
- /**
33
- * Creates a parser for new expressions.
34
- *
35
- * @param {string} name - The name of the type
36
- * @param {Function} argProcessor - Function to process arguments
37
- * @param {number[]} [defaultArr] - The default array to use if the node is not a valid array
38
- * @returns {Function} - The parser function that extracts the values from the node
39
- */
40
- const createNewExpressionParser = (name, argProcessor, defaultArr = []) => {
41
- return (node) => {
42
- if (node.kind !== ts.SyntaxKind.NewExpression) {
43
- if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
44
- throw new ParsingError(node, `Property "${node.name.text}" is not a ${name}.`);
45
- }
46
- return defaultArr;
47
- }
48
- const args = argProcessor(node.arguments);
49
-
50
- // combine the two arrays together, filling any missing values with the defaultArr
51
- return zipArrays(args, defaultArr);
52
- };
53
- };
54
-
55
- /**
56
- * Parses single arguments directly.
57
- * @param {ts.Node[]} args - The arguments to parse
58
- * @returns {number[]} - The parsed arguments
59
- */
60
- const parseNumberArguments = args => args.map(arg => parseFloatNode(arg, true));
61
-
62
- // Creating specific parser instances
63
- const createNumberArgumentParser = (name, defaultArr = []) => (
64
- createNewExpressionParser(name, parseNumberArguments, defaultArr));
65
-
66
- const createArrayArgumentParser = (name, defaultArr = []) => (
67
- createNewExpressionParser(name, parseArrayLiteral, defaultArr));
68
-
69
- /**
70
- * Map of pc types that can be initialized with values and their corresponding argument parsers.
71
- * eg `new Vec3(1, 2, 3)`
72
- */
73
- const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([
74
- ['Curve', createArrayArgumentParser('Curve')],
75
- ['Vec2', createNumberArgumentParser('Vec2', [0, 0])],
76
- ['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])],
77
- ['Vec4', createNumberArgumentParser('Vec4', [0, 0, 0, 0])],
78
- ['Color', createNumberArgumentParser('Color', [1, 1, 1, 1])],
79
- ['number', getLiteralValue],
80
- ['string', getLiteralValue],
81
- ['boolean', getLiteralValue]
82
- ]);
83
-
84
- /**
85
- * The full set of supported PlayCanvas types.
86
- */
87
- const SUPPORTED_PLAYCANVAS_TYPES = new Set([
88
- 'Curve',
89
- 'Asset',
90
- 'Entity',
91
- 'Vec2',
92
- 'Vec3',
93
- 'Vec4',
94
- 'Color'
95
- ]);
96
-
97
- /**
98
- * Checks if a node is a supported PlayCanvas type.
99
- * @param {ts.Node} node - The node to check
100
- * @returns {boolean} - True if the node is a supported PlayCanvas type
101
- */
102
- const isSupportedPlayCanvasNode = (node) => {
103
- return ts.isClassDeclaration(node) && SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text);
104
- };
105
-
106
- // valid values for asset.type
107
- const EDITOR_ASSET_TYPES = [
108
- 'animation',
109
- 'animstategraph',
110
- 'audio',
111
- 'bundle',
112
- 'binary',
113
- 'container',
114
- 'cubemap',
115
- 'css',
116
- 'font',
117
- 'folder',
118
- 'json',
119
- 'html',
120
- 'gsplat',
121
- 'material',
122
- 'model',
123
- 'render',
124
- 'scene',
125
- 'script',
126
- 'shader',
127
- 'sprite',
128
- 'template',
129
- 'text',
130
- 'texture',
131
- 'textureAtlas',
132
- 'wasm'
133
- ];
134
-
135
- const resourceTypesAsUnion = EDITOR_ASSET_TYPES.map(type => `"${type}"`).join('|');
136
-
137
- /**
138
- * The set of supported block tags and validators
139
- */
140
- const SUPPORTED_BLOCK_TAGS = new Map([
141
- ['resource', resourceTypesAsUnion],
142
- ['color', '"r"|"rg"|"rgb"|"rgba"'],
143
- ['curves', '["x"] | ["x", "y"] | ["x", "y", "z"] | ["x", "z"] | ["y"] | ["y", "z"] | ["z"]'],
144
- ['range', 'number[]'],
145
- ['placeholder', 'string'],
146
- ['precision', 'number'],
147
- ['size', 'number'],
148
- ['step', 'number'],
149
- ['title', 'string'],
150
- ['default', 'any'],
151
- ['enabledif', 'any'],
152
- ['visibleif', 'any']
153
- ]);
154
-
155
- /**
156
- * A mapping function that transforms the Script Parsers internal representation of an attribute to a required output.
157
- * @param {*} attribute - The attribute to map
158
- * @returns {*} - The mapped attribute
159
- */
160
- const mapAttributesToOutput = (attribute) => {
161
-
162
- attribute.type = attribute.typeName.toLowerCase();
163
-
164
- // Flatten the range into min and max
165
- if (attribute.range) {
166
- if (attribute.range.length > 0) {
167
- attribute.min = attribute.range[0];
168
- if (attribute.range.length > 1) {
169
- attribute.max = attribute.range[1];
170
- }
171
- }
172
- delete attribute.range;
173
- }
174
-
175
- // We have a complex type, map to json schema
176
- if (typeof attribute.attributes == 'object') {
177
- attribute.type = 'json';
178
- attribute.schema = Object.keys(attribute.attributes).map((key) => {
179
- return attribute.attributes[key];
180
- });
181
- delete attribute.attributes;
182
- }
183
-
184
- // 'color' type needs to be mapped to 'rgba'
185
- if (attribute.type === 'color') {
186
- attribute.type = 'rgba';
187
- }
188
-
189
- // 'resource' type needs to be mapped to 'assetType'
190
- if (attribute.resource) {
191
- attribute.assetType = attribute.resource;
192
- delete attribute.resource;
193
- }
194
-
195
- // remove enum if it's empty
196
- if (attribute.enum.length === 0) delete attribute.enum;
197
-
198
- // If the attribute has no default value then set it
199
- if (attribute.value === undefined) {
200
- if (attribute.type === 'string') attribute.value = '';
201
- if (attribute.type === 'number') attribute.value = 0;
202
- if (attribute.type === 'boolean') attribute.value = false;
203
- }
204
-
205
- // set the default value
206
- if (attribute.value !== undefined) attribute.default = attribute.default ?? attribute.value;
207
-
208
- // Curve Attributes specifically should not expose a default value if it's an empty array
209
- if (attribute.type === 'curve' && Array.isArray(attribute.value) && attribute.value.length === 0) {
210
- delete attribute.default;
211
- }
212
-
213
- // enabledif is in a {}, so strip it out
214
- if (attribute.enabledif?.startsWith('{') && attribute.enabledif?.endsWith('}')) {
215
- attribute.enabledif = attribute.enabledif.slice(1, -1);
216
- }
217
-
218
- // enabledif is in a {}, so strip it out
219
- if (attribute.visibleif?.startsWith('{') && attribute.visibleif?.endsWith('}')) {
220
- attribute.visibleif = attribute.visibleif.slice(1, -1);
221
- }
222
-
223
- // remove typeName from the output
224
- delete attribute.typeName;
225
- delete attribute.value;
226
-
227
- return attribute;
228
- };
229
-
230
- /**
231
- * A class to parse ESM Scripts and extract attribute metadata from JSDoc comments.
232
- * This class uses the TypeScript compiler API to parse the script and extract the metadata.
233
- */
234
- export class ScriptParser {
235
- /**
236
- * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The TypeScript virtual environment
237
- * @param {number} [maxDepth] - The maximum recursion depth
238
- */
239
- constructor(env, maxDepth = 2) {
240
-
241
- if (!env) {
242
- throw new Error('A TypeScript Environment must be provided to the ScriptParser.');
243
- }
244
-
245
- this.env = env;
246
- this.program = env.languageService.getProgram();
247
- this.typeChecker = this.program.getTypeChecker();
248
- this.maxDepth = maxDepth;
249
-
250
- // Initialize the supported types
251
- const playcanvasTypesFile = this.program.getSourceFile('./playcanvas.d.ts');
252
-
253
- if (!playcanvasTypesFile) {
254
- throw new Error('The PlayCanvas types file could not be found.');
255
- }
256
-
257
- // Return an array of all nodes that are supported PlayCanvas types eg Vec3, Color, etc.
258
- const allSupportedTypes = flatMapAnyNodes(playcanvasTypesFile, isSupportedPlayCanvasNode).map(node => this.typeChecker.getTypeAtLocation(node));
259
-
260
- // Add primitive types
261
- allSupportedTypes.push(this.typeChecker.getStringType());
262
- allSupportedTypes.push(this.typeChecker.getNumberType());
263
- allSupportedTypes.push(this.typeChecker.getBooleanType());
264
-
265
- this.typeSerializerMap = new Map();
266
- this.allSupportedTypesSet = new Set(allSupportedTypes);
267
-
268
- allSupportedTypes.forEach((type) => {
269
- const typeName = this.typeChecker.typeToString(type);
270
- const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);
271
- if (serializer) {
272
- this.typeSerializerMap.set(typeName, serializer);
273
- }
274
- });
275
-
276
- this.attributeParser = new AttributeParser(SUPPORTED_BLOCK_TAGS, env, this.typeSerializerMap);
277
-
278
- }
279
-
280
- /**
281
- * Checks a program for compile errors and returns a list of formatted error messages.
282
- * @param {ValidationError[]} errors - The array to store error messages
283
- * @returns {boolean} - True if the program has no errors, false otherwise
284
- */
285
- validateProgram(errors = []) {
286
-
287
- // Report any compiler errors
288
- const compilerDiagnostics = this.program.getSyntacticDiagnostics();
289
-
290
- if (compilerDiagnostics.length > 0) {
291
- for (const diagnostic of compilerDiagnostics) {
292
- const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
293
- if (diagnostic.file) {
294
- const location = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
295
- errors.push({
296
- message,
297
- file: diagnostic.file.fileName,
298
- line: location.line + 1,
299
- column: location.character + 1,
300
- length: diagnostic.length
301
- });
302
- } else {
303
- errors.push({
304
- message
305
- });
306
- }
307
- }
308
- }
309
-
310
- return errors.length === 0;
311
- }
312
-
313
- /**
314
- * @typedef {Object} ParserOptions
315
- * @property {object} [attributes] - The attributes object to add to
316
- * @property {ParsingError[]} [errors] - The array to store parsing errors
317
- * @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag
318
- * @property {number} [depth] - The current recursion depth
319
- */
320
-
321
- /**
322
- * Extracts Script Attributes from JSDoc style tag from a AST node
323
- *
324
- * @param {import('typescript').Node} node - The TypeScript node to analyze
325
- * @param {ParserOptions} [opts] - The input options
326
- * @returns {Attribute[]} - The extracted attributes;
327
- */
328
- extractAttributes(node, opts = {}) {
329
-
330
- // Default params
331
- const attributes = opts.attributes || {};
332
- const errors = opts.errors ?? [];
333
- const requiresAttributeTag = opts.requiresAttributeTag ?? true;
334
- const depth = opts.depth ?? 0;
335
-
336
- // return early if we've reached the maximum depth
337
- if (depth > 10) {
338
- return attributes;
339
- }
340
-
341
- // Find "/** */" style comments associated with this node.
342
- const members = getJSDocTags(node, this.typeChecker);
343
-
344
- // Parse the comments for attribute metadata
345
- for (const { member, memberName } of members) {
346
-
347
- // Parse the comment for attribute metadata
348
- const attributeMetadata = this.attributeParser.parseAttributeComment(
349
- member,
350
- errors,
351
- requiresAttributeTag
352
- );
353
-
354
- // If we found metadata, extract the type and add it to the found attributes
355
- if (attributeMetadata) {
356
-
357
- const { type, typeName } = attributeMetadata;
358
- const initializer = member?.initializer;
359
-
360
- if (typeName === 'any') {
361
-
362
- const error = new ParsingError(member, `The attribute "${memberName}" is not initialized and does not have a @type tag.`);
363
- errors.push(error);
364
- continue;
365
-
366
- } else if (!this.allSupportedTypesSet.has(type) && attributeMetadata.enum.length === 0) {
367
- // If this is a complex type, but not an enum, we may need to recurse through it's nested nested properties
368
-
369
- // extract attributes for the Types properties
370
- const symbol = type.aliasSymbol || type.symbol;
371
- const typeNode = symbol?.valueDeclaration || symbol.declarations?.[0];
372
-
373
- if (!typeNode) {
374
- // The attribute does not have a valid type
375
- continue;
376
- }
377
-
378
- // Type constraints: Must be either an interface, typedef or initialized object literal, or a jsdoc type literal
379
- const typeIsJSDocTypeDef = ts.isJSDocTypedefTag(typeNode);
380
- const typeIsInterface = hasTag('interface', typeNode) || ts.isInterfaceDeclaration(typeNode);
381
- const isInitialized = !!initializer && ts.isObjectLiteralExpression(initializer);
382
- const isJSDocTypeLiteral = type.symbol.name === '__type';
383
-
384
- // Check if the type is a valid interface
385
- if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) {
386
- const error = new ParsingError(member, `Attribute "${memberName}" is an invalid type.`);
387
- errors.push(error);
388
- continue;
389
- }
390
-
391
- // Extract the attributes from the nested type
392
- const members = typeNode.members ?? // interface members
393
- typeNode.properties ?? // an object literals properties
394
- Array.from(type.symbol.members).map(arr => arr[1].declarations[0]) ?? // a typedefs property declarations
395
- [];
396
-
397
- // If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object
398
- let commentAttributes;
399
- if (typeIsInterface || isInitialized) {
400
- commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
401
- }
402
-
403
- // Iterate through the nested attributes and extract their metadata
404
- const nestedAttributes = members.reduce((attributes, prop) => {
405
-
406
- const attribute = this.attributeParser.getNodeAsAttribute(prop, errors);
407
-
408
- // If the attribute is a supported type, add it to the list of found attributes
409
- if (attribute && this.allSupportedTypesSet.has(attribute.type)) {
410
- attributes[attribute.name] = mapAttributesToOutput(attribute);
411
- }
412
-
413
- return attributes;
414
- }, {});
415
-
416
-
417
- // Merge the property and comment attributes
418
- const mergedAttributes = { ...nestedAttributes, ...commentAttributes };
419
-
420
- // Add the nested attributes to the attribute metadata
421
- attributeMetadata.attributes = mergedAttributes;
422
- }
423
-
424
- // Add the attribute to the list of found attributes
425
- attributes[memberName] = mapAttributesToOutput(attributeMetadata);
426
-
427
- }
428
- }
429
-
430
- return attributes;
431
- }
432
- }
@@ -1,53 +0,0 @@
1
- import { getCombinedModifierFlags, ModifierFlags } from 'typescript';
2
-
3
- /**
4
- * @file Utility functions for parsing Script attributes.
5
- */
6
-
7
-
8
- /**
9
- * Returns a jsdoc tag from a JSDoc comment.
10
- * @param {string} tag - The tag to search for
11
- * @param {import('typescript').Node} doc - The JSDoc comment node
12
- * @returns {import('typescript').Node} | null} - The tag node
13
- */
14
- export const getTagFromJsdoc = (tag, doc) => {
15
- return doc?.tags?.find(doc => doc.tagName.text === tag);
16
- };
17
-
18
- /**
19
- * Checks if a node has a specific JSDoc tag.
20
- * @param {string} tag - The tag to search for
21
- * @param {import("typescript").Node} node - The node to analyze
22
- * @returns {import('typescript').Node} - True if the tag is found
23
- */
24
- export const getTag = (tag, node) => {
25
- return node?.jsDoc?.find(doc => getTagFromJsdoc(tag, doc));
26
- };
27
-
28
- /**
29
- * Checks if a node has a specific JSDoc tag.
30
- * @param {string} tag - The tag to search for
31
- * @param {import("typescript").Node} node - The node to analyze
32
- * @returns {boolean} - True if the tag is found
33
- */
34
- export const hasTag = (tag, node) => {
35
- return !!getTag(tag, node);
36
- };
37
-
38
-
39
- /**
40
- * Checks if the JSDoc node node has an '@interface' tag
41
- * @param {import('typescript').Node} node - The node to analyze
42
- * @returns {boolean} - True if the node is an interface
43
- */
44
- export const isInterface = (node) => {
45
- return hasTag('interface', node);
46
- };
47
-
48
- /**
49
- * Checks if a node is a scriptName property
50
- * @param {import('typescript').Node} member - The node to analyze
51
- * @returns {boolean} - True if the node is a scriptName property
52
- */
53
- export const isStaticMember = member => (getCombinedModifierFlags(member) & ModifierFlags.Static) !== 0;