Skip to content

Commit 9653c89

Browse files
authored
Additional name and script ref in error reporting (#54)
1 parent 061109d commit 9653c89

File tree

5 files changed

+62
-17
lines changed

5 files changed

+62
-17
lines changed

src/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ export class JSDocParser {
194194
endLineNumber: insertPos.line + 1,
195195
endColumn: insertPos.character + 1
196196
}
197-
}
197+
},
198+
finalName
198199
)
199200
);
200201
}
@@ -226,7 +227,7 @@ export class JSDocParser {
226227
// Extract attributes from each script
227228
nodes.forEach((node, name) => {
228229
const opts = results[name] = { attributes: {}, errors: [] };
229-
this.parser.extractAttributes(node, opts);
230+
this.parser.extractAttributes(node, { ...opts, scriptName: name });
230231
});
231232

232233
return [results, errors];
@@ -285,7 +286,7 @@ export class JSDocParser {
285286
type: type.name + (type.array ? '[]' : ''),
286287
start: jsdocNode ? { line: jsdocPos.line + 1, column: jsdocPos.character + 1 } :
287288
{ line: namePos.line + 1, column: namePos.character + 1 },
288-
end: { line: namePos.line + 1, column: namePos.character + name.length + 1 }
289+
end: { line: namePos.line + 1, column: namePos.character + 1 }
289290
});
290291
}
291292

src/parsers/attribute-parser.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ export class AttributeParser {
4040
* @param {ts.Node} node - The node to parse
4141
* @param {ParsingError[]} errors - An array to store any parsing errors
4242
* @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
43+
* @param {string} scriptName - The name of the script this node belongs to
44+
* @param {string} attributeName - The name of the attribute this node belongs to
4345
* @returns {*} - The extracted metadata or null if no metadata was found
4446
*/
45-
parseAttributeComment(node, errors = [], requiresAttributeTag = true) {
47+
parseAttributeComment(node, errors = [], requiresAttributeTag = true, scriptName, attributeName) {
4648

4749
// Check if the comment has an attribute tag
4850
if (!requiresAttributeTag || hasTag('attribute', node)) {
4951

5052
// Fetch the primary attribute metadata
51-
const attribute = this.getNodeAsAttribute(node, errors);
53+
const attribute = this.getNodeAsAttribute(node, errors, scriptName);
5254

5355
if (!attribute) return;
5456

@@ -106,7 +108,7 @@ export class AttributeParser {
106108
} : null;
107109

108110
const errorMessage = supportMessage?.() ?? 'The tag is invalid.';
109-
const parseError = new ParsingError(tag, `Invalid Tag '@${tagName}'`, errorMessage, edit);
111+
const parseError = new ParsingError(tag, `Invalid Tag '@${tagName}'`, errorMessage, edit, scriptName, attributeName);
110112
errors.push(parseError);
111113
}
112114
}
@@ -121,9 +123,10 @@ export class AttributeParser {
121123
*
122124
* @param {ts.Node} node - The node to parse
123125
* @param {ParsingError[]} errors - An array to store any parsing errors
126+
* @param {string} [scriptName] - The name of the script this node belongs to
124127
* @returns {*} - The extracted members
125128
*/
126-
getNodeAsAttribute(node, errors = []) {
129+
getNodeAsAttribute(node, errors = [], scriptName) {
127130

128131
const name = node.name && ts.isIdentifier(node.name) && node.name.text;
129132
const { type, name: typeName, array, isMixedUnion } = getType(node, this.typeChecker);
@@ -138,7 +141,10 @@ export class AttributeParser {
138141
node,
139142
'Invalid Type',
140143
'Mixed literal union types (combining different primitive types like string | number) are not supported. ' +
141-
'Use a union of the same primitive type (e.g., \'1 | 2 | 3\' or \'"a" | "b" | "c"\') or refactor your type.'
144+
'Use a union of the same primitive type (e.g., \'1 | 2 | 3\' or \'"a" | "b" | "c"\') or refactor your type.',
145+
undefined,
146+
scriptName,
147+
name
142148
));
143149
return;
144150
}
@@ -149,6 +155,11 @@ export class AttributeParser {
149155
try {
150156
value = serializer(node.initializer ?? node, this.typeChecker);
151157
} catch (error) {
158+
// Enrich any thrown ParsingError with context
159+
if (error && typeof error === 'object') {
160+
error.scriptName = scriptName;
161+
error.attributeName = name;
162+
}
152163
errors.push(error);
153164
return;
154165
}

src/parsers/parsing-error.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,33 @@
1111
* @param {string} type - The type of the error
1212
* @param {string} message - The description of the error
1313
* @param {Fix} [fix] - The fix for the error
14+
* @param {string} [scriptName] - The name of the script this error belongs to (required)
15+
* @param {string} [attributeName] - The name of the attribute this error belongs to (optional)
1416
*/
1517
export class ParsingError {
16-
constructor(node, type, message, fix) {
18+
constructor(node, type, message, fix, scriptName, attributeName) {
1719
this.node = node; // AST node which caused the error
1820
this.type = type; // Type of the error
1921
this.message = message; // Description of the error
2022
this.fix = fix; // Fix for the error
23+
this.scriptName = scriptName; // Script name context
24+
this.attributeName = attributeName; // Attribute name context (optional)
2125
}
2226

2327
toString() {
2428
// Ensure node.location exists and has the necessary properties
2529
const file = this.node.getSourceFile();
2630
const fileName = file.fileName;
2731
const start = this.node.getSourceFile().getLineAndCharacterOfPosition(this.node.getStart());
28-
return `ParsingError: ${this.message} at ${fileName}:${start.line + 1}:${start.character}`;
32+
const location = `${fileName}:${start.line + 1}:${start.character}`;
33+
let context = '';
34+
if (this.scriptName && this.attributeName) {
35+
context = ` [${this.scriptName}.${this.attributeName}]`;
36+
} else if (this.scriptName) {
37+
context = ` [${this.scriptName}]`;
38+
} else if (this.attributeName) {
39+
context = ` [${this.attributeName}]`;
40+
}
41+
return `ParsingError: ${this.message}${context} at ${location}`;
2942
}
3043
}

src/parsers/script-parser.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const createNewExpressionParser = (name, argProcessor, defaultArr = []) => {
4141
return (node) => {
4242
if (node.kind !== ts.SyntaxKind.NewExpression) {
4343
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
44+
// scriptName/attributeName will be filled by caller if caught
4445
throw new ParsingError(node, 'Invalid Property', `Property "${node.name.text}" is not a ${name}.`);
4546
}
4647
return defaultArr;
@@ -353,6 +354,7 @@ export class ScriptParser {
353354
* @property {ParsingError[]} [errors] - The array to store parsing errors
354355
* @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag
355356
* @property {number} [depth] - The current recursion depth
357+
* @property {string} [scriptName] - The current script name for context
356358
*/
357359

358360
/**
@@ -369,6 +371,7 @@ export class ScriptParser {
369371
const errors = opts.errors ?? [];
370372
const requiresAttributeTag = opts.requiresAttributeTag ?? true;
371373
const depth = opts.depth ?? 0;
374+
const scriptName = opts.scriptName ?? undefined;
372375

373376
// return early if we've reached the maximum depth
374377
if (depth > 10) {
@@ -385,7 +388,9 @@ export class ScriptParser {
385388
const attributeMetadata = this.attributeParser.parseAttributeComment(
386389
member,
387390
errors,
388-
requiresAttributeTag
391+
requiresAttributeTag,
392+
scriptName,
393+
memberName
389394
);
390395

391396
// If we found metadata, extract the type and add it to the found attributes
@@ -404,7 +409,7 @@ export class ScriptParser {
404409
} else {
405410
errorMessage = `This attribute has an invalid @type tag. An attribute should be one of the following: ${supportedTypes}.`;
406411
}
407-
const error = new ParsingError(member, 'Invalid Type', errorMessage);
412+
const error = new ParsingError(member, 'Invalid Type', errorMessage, node.name.getText(), memberName, undefined);
408413
errors.push(error);
409414
continue;
410415

@@ -438,23 +443,32 @@ export class ScriptParser {
438443
error = new ParsingError(
439444
typeTag.typeExpression,
440445
'Invalid Type',
441-
`"${typeTag.typeExpression.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`
446+
`"${typeTag.typeExpression.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`,
447+
undefined,
448+
node.name.getText(),
449+
memberName
442450
);
443451

444452
} else if (initializer) {
445453
// If the attribute is initialized with an invalid type, add an error associated with the initializer
446454
error = new ParsingError(
447455
initializer,
448456
'Invalid Type',
449-
`"${initializer.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`
457+
`"${initializer.getText()}" is not a valid attribute type. An attribute should be one of the following: ${supportedTypes}`,
458+
undefined,
459+
node.name.getText(),
460+
memberName
450461
);
451462

452463
} else {
453464
// If the attribute does not have a type tag or initializer, add an error that references the member
454465
error = new ParsingError(
455466
member,
456467
'Invalid Type',
457-
`Attribute is an invalid type. An attribute should be one of the following: ${supportedTypes}`
468+
`Attribute is an invalid type. An attribute should be one of the following: ${supportedTypes}`,
469+
undefined,
470+
node.name.getText(),
471+
memberName
458472
);
459473
}
460474

@@ -471,13 +485,13 @@ export class ScriptParser {
471485
// If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object
472486
let commentAttributes;
473487
if (typeIsInterface || isInitialized) {
474-
commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
488+
commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1, scriptName });
475489
}
476490

477491
// Iterate through the nested attributes and extract their metadata
478492
const nestedAttributes = members.reduce((attributes, prop) => {
479493

480-
const attribute = this.attributeParser.getNodeAsAttribute(prop, errors);
494+
const attribute = this.attributeParser.getNodeAsAttribute(prop, errors, scriptName);
481495

482496
// If the attribute is a supported type, add it to the list of found attributes
483497
if (attribute && this.allSupportedTypesSet.has(attribute.type)) {

test/tests/valid/export.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ describe('VALID: Export Script', function () {
2121
expect(data[1][1].type).to.equal('Missing Script Name');
2222
expect(data[1][2].type).to.equal('Missing Script Name');
2323
expect(data[1][3].type).to.equal('Missing Script Name');
24+
25+
// Ensure scriptName is present on each error
26+
expect(data[1][0].scriptName).to.be.a('string');
27+
expect(data[1][1].scriptName).to.be.a('string');
28+
expect(data[1][2].scriptName).to.be.a('string');
29+
expect(data[1][3].scriptName).to.be.a('string');
2430
});
2531

2632
it('Example: should exist with attributes', function () {

0 commit comments

Comments
 (0)