-
-
Notifications
You must be signed in to change notification settings - Fork 169
Expand file tree
/
Copy pathjsdocUtils.js
More file actions
2158 lines (1920 loc) · 56.3 KB
/
jsdocUtils.js
File metadata and controls
2158 lines (1920 loc) · 56.3 KB
Edit and raw actions
OlderNewer
1
import getDefaultTagStructureForMode from './getDefaultTagStructureForMode.js';
2
import {
3
closureTags,
4
jsdocTags,
5
typeScriptTags,
6
} from './tagNames.js';
7
import WarnSettings from './WarnSettings.js';
8
import {
9
stringify,
10
tryParse,
11
} from '@es-joy/jsdoccomment';
12
13
/**
14
* @typedef {number} Integer
15
*/
16
/**
17
* @typedef {import('./utils/hasReturnValue.js').ESTreeOrTypeScriptNode} ESTreeOrTypeScriptNode
18
*/
19
20
/**
21
* @typedef {"jsdoc"|"typescript"|"closure"|"permissive"} ParserMode
22
*/
23
24
/**
25
* @type {import('./getDefaultTagStructureForMode.js').TagStructure}
26
*/
27
let tagStructure;
28
29
/**
30
* @param {ParserMode} mode
31
* @returns {void}
32
*/
33
const setTagStructure = (mode) => {
34
tagStructure = getDefaultTagStructureForMode(mode);
35
};
36
37
/**
38
* @typedef {undefined|string|{
39
* name: Integer,
40
* restElement: boolean
41
* }|{
42
* isRestProperty: boolean|undefined,
43
* name: string,
44
* restElement: boolean
45
* }|{
46
* name: string,
47
* restElement: boolean
48
* }} ParamCommon
49
*/
50
/**
51
* @typedef {ParamCommon|[string|undefined, (FlattendRootInfo & {
52
* annotationParamName?: string,
53
* })]|NestedParamInfo} ParamNameInfo
54
*/
55
56
/**
57
* @typedef {{
58
* hasPropertyRest: boolean,
59
* hasRestElement: boolean,
60
* names: string[],
61
* rests: boolean[],
62
* }} FlattendRootInfo
63
*/
64
/**
65
* @typedef {[string, (string[]|ParamInfo[])]} NestedParamInfo
66
*/
67
/**
68
* @typedef {ParamCommon|
69
* [string|undefined, (FlattendRootInfo & {
70
* annotationParamName?: string
71
* })]|
72
* NestedParamInfo} ParamInfo
73
*/
74
75
/**
76
* Given a nested array of property names, reduce them to a single array,
77
* appending the name of the root element along the way if present.
78
* @callback FlattenRoots
79
* @param {ParamInfo[]} params
80
* @param {string} [root]
81
* @returns {FlattendRootInfo}
82
*/
83
84
/** @type {FlattenRoots} */
85
const flattenRoots = (params, root = '') => {
86
let hasRestElement = false;
87
let hasPropertyRest = false;
88
89
/**
90
* @type {boolean[]}
91
*/
92
const rests = [];
93
94
const names = params.reduce(
95
/**
96
* @param {string[]} acc
97
* @param {ParamInfo} cur
98
* @returns {string[]}
99
*/
100
(acc, cur) => {
101
if (Array.isArray(cur)) {
102
let nms;
103
if (Array.isArray(cur[1])) {
104
nms = cur[1];
105
} else {
106
if (cur[1].hasRestElement) {
107
hasRestElement = true;
108
}
109
110
if (cur[1].hasPropertyRest) {
111
hasPropertyRest = true;
112
}
113
114
nms = cur[1].names;
115
}
116
117
const flattened = flattenRoots(nms, root ? `${root}.${cur[0]}` : cur[0]);
118
if (flattened.hasRestElement) {
119
hasRestElement = true;
120
}
121
122
if (flattened.hasPropertyRest) {
123
hasPropertyRest = true;
124
}
125
126
const inner = /** @type {string[]} */ ([
127
root ? `${root}.${cur[0]}` : cur[0],
128
...flattened.names,
129
].filter(Boolean));
130
rests.push(false, ...flattened.rests);
131
132
return acc.concat(inner);
133
}
134
135
if (typeof cur === 'object') {
136
if ('isRestProperty' in cur && cur.isRestProperty) {
137
hasPropertyRest = true;
138
rests.push(true);
139
} else {
140
rests.push(false);
141
}
142
143
if ('restElement' in cur && cur.restElement) {
144
hasRestElement = true;
145
}
146
147
acc.push(root ? `${root}.${String(cur.name)}` : String(cur.name));
148
} else if (typeof cur !== 'undefined') {
149
rests.push(false);
150
acc.push(root ? `${root}.${cur}` : cur);
151
}
152
153
return acc;
154
}, [],
155
);
156
157
return {
158
hasPropertyRest,
159
hasRestElement,
160
names,
161
rests,
162
};
163
};
164
165
/**
166
* @param {import('@typescript-eslint/types').TSESTree.TSIndexSignature|
167
* import('@typescript-eslint/types').TSESTree.TSConstructSignatureDeclaration|
168
* import('@typescript-eslint/types').TSESTree.TSCallSignatureDeclaration|
169
* import('@typescript-eslint/types').TSESTree.TSPropertySignature} propSignature
170
* @returns {undefined|string|[string, string[]]}
171
*/
172
const getPropertiesFromPropertySignature = (propSignature) => {
173
if (
174
propSignature.type === 'TSIndexSignature' ||
175
propSignature.type === 'TSConstructSignatureDeclaration' ||
176
propSignature.type === 'TSCallSignatureDeclaration'
177
) {
178
return undefined;
179
}
180
181
if (propSignature.typeAnnotation && propSignature.typeAnnotation.typeAnnotation.type === 'TSTypeLiteral') {
182
return [
183
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
184
propSignature.key
185
).name,
186
propSignature.typeAnnotation.typeAnnotation.members.map((member) => {
187
return /** @type {string} */ (
188
getPropertiesFromPropertySignature(
189
/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (
190
member
191
),
192
)
193
);
194
}),
195
];
196
}
197
198
return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
199
propSignature.key
200
).name;
201
};
202
203
/**
204
* @param {ESTreeOrTypeScriptNode|null} functionNode
205
* @param {boolean} [checkDefaultObjects]
206
* @param {boolean} [ignoreInterfacedParameters]
207
* @throws {Error}
208
* @returns {ParamNameInfo[]}
209
*/
210
const getFunctionParameterNames = (
211
functionNode, checkDefaultObjects, ignoreInterfacedParameters,
212
) => {
213
/* eslint-disable complexity -- Temporary */
214
/**
215
* @param {import('estree').Identifier|import('estree').AssignmentPattern|
216
* import('estree').ObjectPattern|import('estree').Property|
217
* import('estree').RestElement|import('estree').ArrayPattern|
218
* import('@typescript-eslint/types').TSESTree.TSParameterProperty|
219
* import('@typescript-eslint/types').TSESTree.Property|
220
* import('@typescript-eslint/types').TSESTree.RestElement|
221
* import('@typescript-eslint/types').TSESTree.Identifier|
222
* import('@typescript-eslint/types').TSESTree.ObjectPattern|
223
* import('@typescript-eslint/types').TSESTree.BindingName|
224
* import('@typescript-eslint/types').TSESTree.Parameter
225
* } param
226
* @param {boolean} [isProperty]
227
* @returns {ParamNameInfo|[string, ParamNameInfo[]]}
228
*/
229
const getParamName = (param, isProperty) => {
230
/* eslint-enable complexity -- Temporary */
231
const hasLeftTypeAnnotation = 'left' in param && 'typeAnnotation' in param.left;
232
233
if ('typeAnnotation' in param || hasLeftTypeAnnotation) {
234
if (ignoreInterfacedParameters && 'typeAnnotation' in param &&
235
param.typeAnnotation) {
236
// No-op
237
return [
238
undefined, {
239
hasPropertyRest: false,
240
hasRestElement: false,
241
names: [],
242
rests: [],
243
},
244
];
245
}
246
247
const typeAnnotation = hasLeftTypeAnnotation ?
248
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
249
param.left
250
).typeAnnotation :
251
/** @type {import('@typescript-eslint/types').TSESTree.Identifier|import('@typescript-eslint/types').TSESTree.ObjectPattern} */
252
(param).typeAnnotation;
253
254
if (typeAnnotation?.typeAnnotation?.type === 'TSTypeLiteral') {
255
const propertyNames = typeAnnotation.typeAnnotation.members.map((member) => {
256
return getPropertiesFromPropertySignature(
257
/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */
258
(member),
259
);
260
});
261
262
const flattened = {
263
...flattenRoots(propertyNames),
264
annotationParamName: 'name' in param ? param.name : undefined,
265
};
266
const hasLeftName = 'left' in param && 'name' in param.left;
267
268
if ('name' in param || hasLeftName) {
269
return [
270
hasLeftName ?
271
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
272
param.left
273
).name :
274
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
275
param
276
).name,
277
flattened,
278
];
279
}
280
281
return [
282
undefined, flattened,
283
];
284
}
285
}
286
287
if ('name' in param) {
288
return param.name;
289
}
290
291
if ('left' in param && 'name' in param.left) {
292
return param.left.name;
293
}
294
295
if (
296
param.type === 'ObjectPattern' ||
297
('left' in param &&
298
(
299
param
300
).left.type === 'ObjectPattern')
301
) {
302
const properties = /** @type {import('@typescript-eslint/types').TSESTree.ObjectPattern} */ (
303
param
304
).properties ||
305
/** @type {import('estree').ObjectPattern} */
306
(
307
/** @type {import('@typescript-eslint/types').TSESTree.AssignmentPattern} */ (
308
param
309
).left
310
)?.properties;
311
const roots = properties.map((prop) => {
312
return getParamName(prop, true);
313
});
314
315
return [
316
undefined, flattenRoots(roots),
317
];
318
}
319
320
if (param.type === 'Property') {
321
switch (param.value.type) {
322
case 'ArrayPattern': {
323
return [
324
/** @type {import('estree').Identifier} */
325
(param.key).name,
326
/** @type {import('estree').ArrayPattern} */ (
327
param.value
328
).elements.map((prop, idx) => {
329
return {
330
name: idx,
331
restElement: prop?.type === 'RestElement',
332
};
333
}),
334
];
335
}
336
337
case 'ObjectPattern': {
338
return [
339
/** @type {import('estree').Identifier} */ (param.key).name,
340
/** @type {import('estree').ObjectPattern} */ (
341
param.value
342
).properties.map((prop) => {
343
return /** @type {string|[string, string[]]} */ (getParamName(prop, isProperty));
344
}),
345
];
346
}
347
348
case 'AssignmentPattern': {
349
switch (param.value.left.type) {
350
case 'ArrayPattern':
351
return [
352
/** @type {import('estree').Identifier} */
353
(param.key).name,
354
/** @type {import('estree').ArrayPattern} */ (
355
param.value.left
356
).elements.map((prop, idx) => {
357
return {
358
name: idx,
359
restElement: prop?.type === 'RestElement',
360
};
361
}),
362
];
363
case 'Identifier':
364
// Default parameter
365
if (checkDefaultObjects && param.value.right.type === 'ObjectExpression') {
366
return [
367
/** @type {import('estree').Identifier} */ (
368
param.key
369
).name,
370
/** @type {import('estree').AssignmentPattern} */ (
371
param.value
372
).right.properties.map((prop) => {
373
return /** @type {string} */ (getParamName(
374
/** @type {import('estree').Property} */
375
(prop),
376
isProperty,
377
));
378
}),
379
];
380
}
381
382
break;
383
case 'ObjectPattern':
384
return [
385
/** @type {import('estree').Identifier} */
386
(param.key).name,
387
/** @type {import('estree').ObjectPattern} */ (
388
param.value.left
389
).properties.map((prop) => {
390
return getParamName(prop, isProperty);
391
}),
392
];
393
}
394
}
395
}
396
397
switch (param.key.type) {
398
case 'Identifier':
399
return param.key.name;
400
401
// The key of an object could also be a string or number
402
case 'Literal':
403
/* c8 ignore next 2 -- `raw` may not be present in all parsers */
404
return /** @type {string} */ (param.key.raw ||
405
param.key.value);
406
407
// case 'MemberExpression':
408
default:
409
// Todo: We should really create a structure (and a corresponding
410
// option analogous to `checkRestProperty`) which allows for
411
// (and optionally requires) dynamic properties to have a single
412
// line of documentation
413
return undefined;
414
}
415
}
416
417
if (
418
param.type === 'ArrayPattern' ||
419
/** @type {import('estree').AssignmentPattern} */ (
420
param
421
).left?.type === 'ArrayPattern'
422
) {
423
const elements = /** @type {import('estree').ArrayPattern} */ (
424
param
425
).elements || /** @type {import('estree').ArrayPattern} */ (
426
/** @type {import('estree').AssignmentPattern} */ (
427
param
428
).left
429
)?.elements;
430
const roots = elements.map((prop, idx) => {
431
return {
432
name: `"${idx}"`,
433
restElement: prop?.type === 'RestElement',
434
};
435
});
436
437
return [
438
undefined, flattenRoots(roots),
439
];
440
}
441
442
if ([
443
'ExperimentalRestProperty', 'RestElement',
444
].includes(param.type)) {
445
return {
446
isRestProperty: isProperty,
447
name: /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
448
/** @type {import('@typescript-eslint/types').TSESTree.RestElement} */ (
449
param
450
// @ts-expect-error Ok
451
).argument).name ?? param?.argument?.elements?.map(({
452
// @ts-expect-error Ok
453
name,
454
}) => {
455
return name;
456
}),
457
restElement: true,
458
};
459
}
460
461
if (param.type === 'TSParameterProperty') {
462
return getParamName(
463
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
464
/** @type {import('@typescript-eslint/types').TSESTree.TSParameterProperty} */ (
465
param
466
).parameter
467
),
468
true,
469
);
470
}
471
472
throw new Error(`Unsupported function signature format: \`${param.type}\`.`);
473
};
474
475
if (!functionNode) {
476
return [];
477
}
478
479
return (
480
/** @type {import('@typescript-eslint/types').TSESTree.TSFunctionType} */
481
(/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (functionNode)?.typeAnnotation?.typeAnnotation)?.params ||
482
/** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
483
functionNode
484
).params || /** @type {import('@typescript-eslint/types').TSESTree.MethodDefinition} */ (
485
functionNode
486
).value?.params || []).map((param) => {
487
return getParamName(param);
488
});
489
};
490
491
/**
492
* @param {ESTreeOrTypeScriptNode} functionNode
493
* @returns {Integer}
494
*/
495
const hasParams = (functionNode) => {
496
// Should also check `functionNode.value.params` if supporting `MethodDefinition`
497
return /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
498
functionNode
499
).params.length;
500
};
501
502
/**
503
* Gets all names of the target type, including those that refer to a path, e.g.
504
* `foo` or `foo.bar`.
505
* @param {import('comment-parser').Block} jsdoc
506
* @param {string} targetTagName
507
* @returns {{
508
* idx: Integer,
509
* name: string,
510
* type: string
511
* }[]}
512
*/
513
const getJsdocTagsDeep = (jsdoc, targetTagName) => {
514
const ret = [];
515
for (const [
516
idx,
517
{
518
name,
519
tag,
520
type,
521
},
522
] of jsdoc.tags.entries()) {
523
if (tag !== targetTagName) {
524
continue;
525
}
526
527
ret.push({
528
idx,
529
name,
530
type,
531
});
532
}
533
534
return ret;
535
};
536
537
const modeWarnSettings = WarnSettings();
538
539
/**
540
* @param {ParserMode|undefined} mode
541
* @param {import('eslint').Rule.RuleContext} context
542
* @returns {import('./tagNames.js').AliasedTags}
543
*/
544
const getTagNamesForMode = (mode, context) => {
545
switch (mode) {
546
case 'closure':
547
case 'permissive':
548
return closureTags;
549
case 'jsdoc':
550
return jsdocTags;
551
case 'typescript':
552
return typeScriptTags;
553
default:
554
if (!modeWarnSettings.hasBeenWarned(context, 'mode')) {
555
context.report({
556
loc: {
557
end: {
558
column: 1,
559
line: 1,
560
},
561
start: {
562
column: 1,
563
line: 1,
564
},
565
},
566
message: `Unrecognized value \`${mode}\` for \`settings.jsdoc.mode\`.`,
567
});
568
modeWarnSettings.markSettingAsWarned(context, 'mode');
569
}
570
571
// We'll avoid breaking too many other rules
572
return jsdocTags;
573
}
574
};
575
576
/**
577
* @param {import('comment-parser').Spec} tg
578
* @param {boolean} [returnArray]
579
* @returns {string[]|string}
580
*/
581
const getTagDescription = (tg, returnArray) => {
582
/**
583
* @type {string[]}
584
*/
585
const descriptions = [];
586
tg.source.some(({
587
tokens: {
588
description,
589
end,
590
lineEnd,
591
name,
592
postDelimiter,
593
postTag,
594
tag,
595
type,
596
},
597
}) => {
598
const desc = (
599
tag && postTag ||
600
!tag && !name && !type && postDelimiter || ''
601
602
// Remove space
603
).slice(1) +
604
(description || '') + (lineEnd || '');
605
606
if (end) {
607
if (desc) {
608
descriptions.push(desc);
609
}
610
611
return true;
612
}
613
614
descriptions.push(desc);
615
616
return false;
617
});
618
619
return returnArray ? descriptions : descriptions.join('\n');
620
};
621
622
/**
623
* @typedef {{
624
* report: (descriptor: import('eslint').Rule.ReportDescriptor) => void
625
* }} Reporter
626
*/
627
628
/**
629
* @param {string} name
630
* @param {ParserMode|undefined} mode
631
* @param {TagNamePreference} tagPreference
632
* @param {import('eslint').Rule.RuleContext} context
633
* @returns {string|false|{
634
* message: string;
635
* replacement?: string|undefined;
636
* }}
637
*/
638
const getPreferredTagNameSimple = (
639
name,
640
mode,
641
tagPreference = {},
642
// @ts-expect-error Just a no-op
643
// eslint-disable-next-line unicorn/no-object-as-default-parameter -- Ok
644
context = {
645
report () {
646
// No-op
647
},
648
},
649
) => {
650
const prefValues = Object.values(tagPreference);
651
if (prefValues.includes(name) || prefValues.some((prefVal) => {
652
return prefVal && typeof prefVal === 'object' && prefVal.replacement === name;
653
})) {
654
return name;
655
}
656
657
// Allow keys to have a 'tag ' prefix to avoid upstream bug in ESLint
658
// that disallows keys that conflict with Object.prototype,
659
// e.g. 'tag constructor' for 'constructor':
660
// https://github.com/eslint/eslint/issues/13289
661
// https://github.com/gajus/eslint-plugin-jsdoc/issues/537
662
const tagPreferenceFixed = Object.fromEntries(
663
Object
664
.entries(tagPreference)
665
.map(([
666
key,
667
value,
668
]) => {
669
return [
670
key.replace(/^tag /v, ''), value,
671
];
672
}),
673
);
674
675
if (Object.hasOwn(tagPreferenceFixed, name)) {
676
return tagPreferenceFixed[name];
677
}
678
679
const tagNames = getTagNamesForMode(mode, context);
680
681
const preferredTagName = Object.entries(tagNames).find(([
682
, aliases,
683
]) => {
684
return aliases.includes(name);
685
})?.[0];
686
if (preferredTagName) {
687
return preferredTagName;
688
}
689
690
return name;
691
};
692
693
/**
694
* @param {import('eslint').Rule.RuleContext} context
695
* @param {ParserMode|undefined} mode
696
* @param {string} name
697
* @param {string[]} definedTags
698
* @returns {boolean}
699
*/
700
const isValidTag = (
701
context,
702
mode,
703
name,
704
definedTags,
705
) => {
706
const tagNames = getTagNamesForMode(mode, context);
707
708
const validTagNames = Object.keys(tagNames).concat(Object.values(tagNames).flat());
709
const additionalTags = definedTags;
710
const allTags = validTagNames.concat(additionalTags);
711
712
return allTags.includes(name);
713
};
714
715
/**
716
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
717
* @param {string} targetTagName
718
* @returns {boolean}
719
*/
720
const hasTag = (jsdoc, targetTagName) => {
721
const targetTagLower = targetTagName.toLowerCase();
722
723
return jsdoc.tags.some((doc) => {
724
return doc.tag.toLowerCase() === targetTagLower;
725
});
726
};
727
728
/**
729
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
730
* @param {(tag: import('@es-joy/jsdoccomment').JsdocTagWithInline) => boolean} filter
731
* @returns {import('@es-joy/jsdoccomment').JsdocTagWithInline[]}
732
*/
733
const filterTags = (jsdoc, filter) => {
734
return jsdoc.tags.filter((tag) => {
735
return filter(tag);
736
});
737
};
738
739
/**
740
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
741
* @param {string} tagName
742
* @returns {import('comment-parser').Spec[]}
743
*/
744
const getTags = (jsdoc, tagName) => {
745
return filterTags(jsdoc, (item) => {
746
return item.tag === tagName;
747
});
748
};
749
750
/**
751
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
752
* @param {{
753
* tagName: string,
754
* context?: import('eslint').Rule.RuleContext,
755
* mode?: ParserMode,
756
* report?: import('./iterateJsdoc.js').Report
757
* tagNamePreference?: TagNamePreference
758
* skipReportingBlockedTag?: boolean,
759
* allowObjectReturn?: boolean,
760
* defaultMessage?: string,
761
* }} cfg
762
* @returns {string|undefined|false|{
763
* message: string;
764
* replacement?: string|undefined;
765
* }|{
766
* blocked: true,
767
* tagName: string
768
* }}
769
*/
770
const getPreferredTagName = (jsdoc, {
771
allowObjectReturn = false,
772
context,
773
tagName,
774
defaultMessage = `Unexpected tag \`@${tagName}\``,
775
mode,
776
report = () => {},
777
skipReportingBlockedTag = false,
778
tagNamePreference,
779
}) => {
780
const ret = getPreferredTagNameSimple(tagName, mode, tagNamePreference, context);
781
const isObject = ret && typeof ret === 'object';
782
if (hasTag(jsdoc, tagName) && (ret === false || isObject && !ret.replacement)) {
783
if (skipReportingBlockedTag) {
784
return {
785
blocked: true,
786
tagName,
787
};
788
}
789
790
const message = isObject && ret.message || defaultMessage;
791
report(message, null, getTags(jsdoc, tagName)[0]);
792
793
return false;
794
}
795
796
return isObject && !allowObjectReturn ? ret.replacement : ret;
797
};
798
799
/**
800
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
801
* @param {string} tagName
802
* @param {(
803
* matchingJsdocTag: import('@es-joy/jsdoccomment').JsdocTagWithInline,
804
* targetTagName: string
805
* ) => void} arrayHandler
806
* @param {object} cfg
807
* @param {import('eslint').Rule.RuleContext} [cfg.context]
808
* @param {ParserMode} [cfg.mode]
809
* @param {import('./iterateJsdoc.js').Report} [cfg.report]
810
* @param {TagNamePreference} [cfg.tagNamePreference]
811
* @param {boolean} [cfg.skipReportingBlockedTag]
812
* @returns {void}
813
*/
814
const forEachPreferredTag = (
815
jsdoc, tagName, arrayHandler,
816
{
817
context,
818
mode,
819
report,
820
skipReportingBlockedTag = false,
821
tagNamePreference,
822
} = {},
823
) => {
824
const targetTagName = /** @type {string|false} */ (
825
getPreferredTagName(jsdoc, {
826
context,
827
mode,
828
report,
829
skipReportingBlockedTag,
830
tagName,
831
tagNamePreference,
832
})
833
);
834
if (!targetTagName ||
835
skipReportingBlockedTag && targetTagName && typeof targetTagName === 'object'
836
) {
837
return;
838
}
839
840
const matchingJsdocTags = jsdoc.tags.filter(({
841
tag,
842
}) => {
843
return tag === targetTagName;
844
});
845
846
for (const matchingJsdocTag of matchingJsdocTags) {
847
arrayHandler(
848
/**
849
* @type {import('@es-joy/jsdoccomment').JsdocTagWithInline}
850
*/ (
851
matchingJsdocTag
852
), targetTagName,
853
);
854
}
855
};
856
857
/**
858
* Get all inline tags and inline tags in tags
859
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
860
* @returns {(import('comment-parser').Spec|
861
* import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
862
* line?: number | undefined; column?: number | undefined;
863
* })[]}
864
*/
865
const getInlineTags = (jsdoc) => {
866
return [
867
...jsdoc.inlineTags.map((inlineTag) => {
868
// Tags don't have source or line numbers, so add before returning
869
let line = -1;
870
for (const {
871
tokens: {
872
description,
873
},
874
} of jsdoc.source) {
875
line++;
876
if (description && description.includes(`{@${inlineTag.tag}`)) {
877
break;
878
}
879
}
880
881
inlineTag.line = line;
882
883
return inlineTag;
884
}),
885
...jsdoc.tags.flatMap((tag) => {
886
for (const inlineTag of tag.inlineTags) {
887
/** @type {import('./iterateJsdoc.js').Integer} */
888
let line = 0;
889
for (const {
890
number,
891
tokens: {
892
description,
893
},
894
} of tag.source) {
895
if (description && description.includes(`{@${inlineTag.tag}`)) {
896
line = number;
897
break;
898
}
899
}
900
901
inlineTag.line = line;
902
}
903
904
return (
905
/**
906
* @type {import('comment-parser').Spec & {
907
* inlineTags: import('@es-joy/jsdoccomment').JsdocInlineTagNoType[]
908
* }}
909
*/ (
910
tag
911
).inlineTags
912
);
913
}),
914
];
915
};
916
917
/**
918
* Get all tags, inline tags and inline tags in tags
919
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
920
* @returns {(import('comment-parser').Spec|
921
* import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
922
* line?: number | undefined; column?: number | undefined;
923
* })[]}
924
*/
925
const getAllTags = (jsdoc) => {
926
return [
927
...jsdoc.tags,
928
...getInlineTags(jsdoc),
929
];
930
};
931
932
/**
933
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
934
* @param {string[]} targetTagNames
935
* @returns {boolean}
936
*/
937
const hasATag = (jsdoc, targetTagNames) => {
938
return targetTagNames.some((targetTagName) => {
939
return hasTag(jsdoc, targetTagName);
940
});
941
};
942
943
/**
944
* Checks if the JSDoc comment has an undefined type.
945
* @param {import('comment-parser').Spec|null|undefined} tag
946
* the tag which should be checked.
947
* @param {ParserMode} mode
948
* @returns {boolean}
949
* true in case a defined type is undeclared; otherwise false.
950
*/
951
const mayBeUndefinedTypeTag = (tag, mode) => {
952
// The function should not continue in the event the type is not defined...
953
if (typeof tag === 'undefined' || tag === null) {
954
return true;
955
}
956
957
// .. same applies if it declares an `{undefined}` or `{void}` type
958
const tagType = tag.type.trim();
959
960
// Exit early if matching
961
if (
962
tagType === 'undefined' || tagType === 'void' ||
963
tagType === '*' || tagType === 'any'
964
) {
965
return true;
966
}
967
968
let parsedTypes;
969
try {
970
parsedTypes = tryParse(
971
tagType,
972
mode === 'permissive' ? undefined : [
973
mode,
974
],
975
);
976
} catch {
977
// Ignore
978
}
979
980
if (
981
// We do not traverse deeply as it could be, e.g., `Promise<void>`
982
parsedTypes &&
983
parsedTypes.type === 'JsdocTypeUnion' &&
984
parsedTypes.elements.some((elem) => {
985
return elem.type === 'JsdocTypeUndefined' ||
986
elem.type === 'JsdocTypeName' && elem.value === 'void';
987
})) {
988
return true;
989
}
990
991
// In any other case, a type is present
992
return false;
993
};
994
995
/**
996
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} map
997
* @param {string} tag
998
* @returns {Map<string, string|string[]|boolean|undefined>}
999
*/
1000
const ensureMap = (map, tag) => {