eslint-plugin-jsdoc 63.0.13 → 63.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cjs/rules/normalizeSeeLinks.d.ts +2 -0
- package/dist/index-cjs.cjs +3 -0
- package/dist/index-cjs.cjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/rules/escapeInlineTags.cjs +24 -1
- package/dist/rules/escapeInlineTags.cjs.map +1 -1
- package/dist/rules/normalizeSeeLinks.cjs +204 -0
- package/dist/rules/normalizeSeeLinks.cjs.map +1 -0
- package/dist/rules/normalizeSeeLinks.d.ts +3 -0
- package/dist/rules.d.ts +16 -0
- package/package.json +1 -1
- package/src/index-cjs.js +3 -0
- package/src/index.js +3 -0
- package/src/rules/escapeInlineTags.js +31 -2
- package/src/rules/normalizeSeeLinks.js +283 -0
- package/src/rules.d.ts +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizeSeeLinks.cjs","names":["iterateJsdoc"],"sources":["../../src/rules/normalizeSeeLinks.js"],"sourcesContent":["import iterateJsdoc from '../iterateJsdoc.js';\n\nconst markdownLinkRegex = /(?<!\\\\)\\[([^`\\]\\r\\n]+)\\]\\(([^`\\(\\)\\s]+)\\)/gv;\nconst prefixLinkRegex = /(?<!\\\\)\\[([^\\]\\r\\n]+)\\]\\{@link\\s+([^\\}\\s\\|]+)\\}/gv;\nconst pipeLinkRegex = /\\{@link\\s+([^\\}\\s\\|]+)\\s*\\|\\s*([^\\}\\r\\n]+)\\}/gv;\n\nconst markdownLinkAttemptRegex = /(?<!\\\\)\\[[^\\]\\r\\n]*\\]\\([^\\r\\n]*(?:\\)|$)/v;\nconst prefixLinkAttemptRegex = /(?<!\\\\)\\[[^\\]\\r\\n]*\\]\\{@link[^\\}\\r\\n]*(?:\\}|$)/v;\nconst pipeLinkAttemptRegex = /\\{@link[^\\}\\r\\n]*\\|[^\\}\\r\\n]*(?:\\}|$)/v;\n\nconst scopedPackageNameRegex = /^@[\\w.\\-]+\\/[\\w.\\-]+/v;\nconst markdownCodeSpanRegex = /(?<!\\\\)(`+)(?!`)[\\s\\S]*?(?<!`)\\1(?!`)/gv;\n\n/**\n * @param {'pipe'|'prefix'} canonicalForm\n * @param {string} label\n * @returns {boolean}\n */\nconst cannotSafelyFormatLink = (canonicalForm, label) => {\n if (!label.trim()) {\n return true;\n }\n\n if (canonicalForm === 'pipe') {\n return label.includes('}');\n }\n\n return label.includes(']');\n};\n\n/**\n * @param {string} target\n * @returns {string}\n */\nconst encodeLinkTarget = (target) => {\n return encodeURI(target).replaceAll(/%25(?=[\\dA-F]{2})/giv, '%');\n};\n\n/**\n * @param {string} description\n * @param {number} index\n * @returns {boolean}\n */\nconst isInsideMarkdownCodeSpan = (description, index) => {\n for (const match of description.matchAll(markdownCodeSpanRegex)) {\n const delimiterLength = match[1].length;\n const contentStart = match.index + delimiterLength;\n const contentEnd = match.index + match[0].length - delimiterLength;\n\n if (index >= contentStart && index < contentEnd) {\n return true;\n }\n }\n\n return false;\n};\n\n/**\n * @param {'pipe'|'prefix'} canonicalForm\n * @param {string} label\n * @param {string} target\n * @returns {string}\n */\nconst formatLink = (canonicalForm, label, target) => {\n const encodedTarget = encodeLinkTarget(target);\n\n return canonicalForm === 'pipe' ?\n `{@link ${encodedTarget}|${label.trim()}}` :\n `[${label.trim()}]{@link ${encodedTarget}}`;\n};\n\n/**\n * @param {string} description\n * @param {'pipe'|'prefix'} canonicalForm\n * @returns {string}\n */\nconst normalizeDescription = (description, canonicalForm) => {\n const markdownNormalized = description.replaceAll(\n new RegExp(markdownLinkRegex, 'gv'),\n (_match, label, target) => {\n return formatLink(canonicalForm, label, target);\n },\n );\n\n if (canonicalForm === 'pipe') {\n return markdownNormalized.replaceAll(\n new RegExp(prefixLinkRegex, 'gv'),\n (_match, label, target) => {\n return formatLink(canonicalForm, label, target);\n },\n );\n }\n\n return markdownNormalized.replaceAll(\n new RegExp(pipeLinkRegex, 'gv'),\n (_match, target, label) => {\n return formatLink(canonicalForm, label, target);\n },\n );\n};\n\nexport default iterateJsdoc(({\n context,\n jsdoc,\n utils,\n}) => {\n const {\n canonicalForm = 'pipe',\n enableFixer = true,\n } = context.options[0] || {};\n\n const descriptionMatcher = canonicalForm === 'pipe' ?\n new RegExp(`${markdownLinkRegex.source}|${prefixLinkRegex.source}`, 'v') :\n new RegExp(`${markdownLinkRegex.source}|${pipeLinkRegex.source}`, 'v');\n\n /** @type {import('comment-parser').Spec[]} */\n const fixableTags = [];\n\n for (const tag of jsdoc.tags) {\n if (tag.tag !== 'see') {\n continue;\n }\n\n const firstTokens = tag.source[0].tokens;\n const tagDescription = String(utils.getTagDescription(tag));\n const rawDescription = firstTokens.name ?\n firstTokens.name + firstTokens.postName + tagDescription :\n tagDescription;\n\n let hasAmbiguousLink = false;\n let hasNoncanonicalLink = false;\n\n for (const match of rawDescription.matchAll(markdownLinkRegex)) {\n const label = match[1];\n const target = match[2];\n\n if (\n isInsideMarkdownCodeSpan(rawDescription, match.index) ||\n scopedPackageNameRegex.test(target) ||\n cannotSafelyFormatLink(canonicalForm, label)\n ) {\n hasAmbiguousLink = true;\n } else {\n hasNoncanonicalLink = true;\n }\n }\n\n for (const inlineTag of tag.inlineTags) {\n if (inlineTag.tag !== 'link' || !inlineTag.text) {\n continue;\n }\n\n if (inlineTag.format === 'space') {\n hasAmbiguousLink = true;\n continue;\n }\n\n if (cannotSafelyFormatLink(\n canonicalForm,\n inlineTag.text,\n )) {\n hasAmbiguousLink = true;\n continue;\n }\n\n if (inlineTag.format === canonicalForm) {\n continue;\n }\n\n const {\n start,\n } = /** @type {typeof inlineTag & {start: number}} */ (inlineTag);\n\n if (\n isInsideMarkdownCodeSpan(rawDescription, start) ||\n scopedPackageNameRegex.test(inlineTag.namepathOrURL)\n ) {\n hasAmbiguousLink = true;\n } else {\n hasNoncanonicalLink = true;\n }\n }\n\n const unrecognizedLinkText = rawDescription\n .replaceAll(new RegExp(markdownLinkRegex, 'gv'), '')\n .replaceAll(new RegExp(prefixLinkRegex, 'gv'), '')\n .replaceAll(new RegExp(pipeLinkRegex, 'gv'), '');\n\n hasAmbiguousLink ||= markdownLinkAttemptRegex.test(unrecognizedLinkText) ||\n prefixLinkAttemptRegex.test(unrecognizedLinkText) ||\n pipeLinkAttemptRegex.test(unrecognizedLinkText);\n\n if (hasAmbiguousLink) {\n utils.reportJSDoc('@see link cannot be safely normalized.', {\n line: tag.source[0].number,\n }, null);\n continue;\n }\n\n if (!hasNoncanonicalLink) {\n continue;\n }\n\n fixableTags.push(tag);\n }\n\n for (const [\n reportIdx,\n tag,\n ] of fixableTags.entries()) {\n utils.reportJSDoc(\n `Expected @see link to use the ${canonicalForm} form.`,\n {\n line: tag.source[0].number,\n },\n enableFixer && reportIdx === 0 ?\n () => {\n for (const fixableTag of fixableTags) {\n const firstTokens = fixableTag.source[0].tokens;\n\n if (firstTokens.name) {\n firstTokens.description = firstTokens.name +\n firstTokens.postName + firstTokens.description;\n firstTokens.name = '';\n firstTokens.postName = '';\n }\n\n for (\n let sourceIdx = 0;\n sourceIdx < fixableTag.source.length;\n sourceIdx++\n ) {\n if (!descriptionMatcher.test(\n fixableTag.source[sourceIdx].tokens.description,\n )) {\n continue;\n }\n\n utils.setTagDescription(\n fixableTag,\n descriptionMatcher,\n (description) => {\n return normalizeDescription(description, canonicalForm);\n },\n );\n }\n }\n } :\n null,\n true,\n );\n }\n}, {\n iterateAllJsdocs: true,\n meta: {\n docs: {\n description: 'Normalizes labeled links in `@see` tags to a canonical `{@link}` form.',\n url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/normalize-see-links.md#repos-sticky-header',\n },\n fixable: 'code',\n schema: [\n {\n additionalProperties: false,\n properties: {\n canonicalForm: {\n description: 'The canonical `{@link}` form: `\"pipe\"` produces `{@link url|label}`, while `\"prefix\"` produces `[label]{@link url}`. Defaults to `\"pipe\"`.',\n enum: [\n 'pipe',\n 'prefix',\n ],\n type: 'string',\n },\n enableFixer: {\n description: 'Whether to enable the fixer. Defaults to `true`.',\n type: 'boolean',\n },\n },\n type: 'object',\n },\n ],\n type: 'suggestion',\n },\n});\n"],"mappings":"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","ignoreList":[]}
|
package/dist/rules.d.ts
CHANGED
|
@@ -1292,6 +1292,22 @@ export interface Rules {
|
|
|
1292
1292
|
}
|
|
1293
1293
|
];
|
|
1294
1294
|
|
|
1295
|
+
/** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
|
|
1296
|
+
"jsdoc/normalize-see-links":
|
|
1297
|
+
| []
|
|
1298
|
+
| [
|
|
1299
|
+
{
|
|
1300
|
+
/**
|
|
1301
|
+
* The canonical `{@link}` form: `"pipe"` produces `{@link url|label}`, while `"prefix"` produces `[label]{@link url}`. Defaults to `"pipe"`.
|
|
1302
|
+
*/
|
|
1303
|
+
canonicalForm?: "pipe" | "prefix";
|
|
1304
|
+
/**
|
|
1305
|
+
* Whether to enable the fixer. Defaults to `true`.
|
|
1306
|
+
*/
|
|
1307
|
+
enableFixer?: boolean;
|
|
1308
|
+
}
|
|
1309
|
+
];
|
|
1310
|
+
|
|
1295
1311
|
/** Prefer `@import` tags to inline `import()` statements. */
|
|
1296
1312
|
"jsdoc/prefer-import-tag":
|
|
1297
1313
|
| []
|
package/package.json
CHANGED
package/src/index-cjs.js
CHANGED
|
@@ -36,6 +36,7 @@ import noDefaults from './rules/noDefaults.js';
|
|
|
36
36
|
import noMissingSyntax from './rules/noMissingSyntax.js';
|
|
37
37
|
import noMultiAsterisks from './rules/noMultiAsterisks.js';
|
|
38
38
|
import noRestrictedSyntax from './rules/noRestrictedSyntax.js';
|
|
39
|
+
import normalizeSeeLinks from './rules/normalizeSeeLinks.js';
|
|
39
40
|
import noTypes from './rules/noTypes.js';
|
|
40
41
|
import noUndefinedTypes from './rules/noUndefinedTypes.js';
|
|
41
42
|
import preferImportTag from './rules/preferImportTag.js';
|
|
@@ -128,6 +129,7 @@ index.rules = {
|
|
|
128
129
|
'no-restricted-syntax': noRestrictedSyntax,
|
|
129
130
|
'no-types': noTypes,
|
|
130
131
|
'no-undefined-types': noUndefinedTypes,
|
|
132
|
+
'normalize-see-links': normalizeSeeLinks,
|
|
131
133
|
'prefer-import-tag': preferImportTag,
|
|
132
134
|
'reject-any-type': buildRejectOrPreferRuleDefinition({
|
|
133
135
|
description: 'Reports use of `any` or `*` type',
|
|
@@ -318,6 +320,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
|
|
|
318
320
|
'jsdoc/no-restricted-syntax': 'off',
|
|
319
321
|
'jsdoc/no-types': 'off',
|
|
320
322
|
'jsdoc/no-undefined-types': warnOrError,
|
|
323
|
+
'jsdoc/normalize-see-links': 'off',
|
|
321
324
|
'jsdoc/prefer-import-tag': 'off',
|
|
322
325
|
'jsdoc/reject-any-type': warnOrError,
|
|
323
326
|
'jsdoc/reject-function-type': warnOrError,
|
package/src/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import noDefaults from './rules/noDefaults.js';
|
|
|
42
42
|
import noMissingSyntax from './rules/noMissingSyntax.js';
|
|
43
43
|
import noMultiAsterisks from './rules/noMultiAsterisks.js';
|
|
44
44
|
import noRestrictedSyntax from './rules/noRestrictedSyntax.js';
|
|
45
|
+
import normalizeSeeLinks from './rules/normalizeSeeLinks.js';
|
|
45
46
|
import noTypes from './rules/noTypes.js';
|
|
46
47
|
import noUndefinedTypes from './rules/noUndefinedTypes.js';
|
|
47
48
|
import preferImportTag from './rules/preferImportTag.js';
|
|
@@ -134,6 +135,7 @@ index.rules = {
|
|
|
134
135
|
'no-restricted-syntax': noRestrictedSyntax,
|
|
135
136
|
'no-types': noTypes,
|
|
136
137
|
'no-undefined-types': noUndefinedTypes,
|
|
138
|
+
'normalize-see-links': normalizeSeeLinks,
|
|
137
139
|
'prefer-import-tag': preferImportTag,
|
|
138
140
|
'reject-any-type': buildRejectOrPreferRuleDefinition({
|
|
139
141
|
description: 'Reports use of `any` or `*` type',
|
|
@@ -324,6 +326,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
|
|
|
324
326
|
'jsdoc/no-restricted-syntax': 'off',
|
|
325
327
|
'jsdoc/no-types': 'off',
|
|
326
328
|
'jsdoc/no-undefined-types': warnOrError,
|
|
329
|
+
'jsdoc/normalize-see-links': 'off',
|
|
327
330
|
'jsdoc/prefer-import-tag': 'off',
|
|
328
331
|
'jsdoc/reject-any-type': warnOrError,
|
|
329
332
|
'jsdoc/reject-function-type': warnOrError,
|
|
@@ -41,6 +41,34 @@ export default iterateJsdoc(({
|
|
|
41
41
|
'linkplain',
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
+
const markdownCodeSpanRegex = /(?<!\\)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/gv;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} desc
|
|
48
|
+
* @param {number} index
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
const isInsideMarkdownCodeSpan = (desc, index) => {
|
|
52
|
+
markdownCodeSpanRegex.lastIndex = 0;
|
|
53
|
+
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = markdownCodeSpanRegex.exec(desc)) !== null) {
|
|
56
|
+
const delimiterLength = match[1].length;
|
|
57
|
+
const contentStart = match.index + delimiterLength;
|
|
58
|
+
const contentEnd = match.index + match[0].length - delimiterLength;
|
|
59
|
+
|
|
60
|
+
if (index < contentStart) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (index < contentEnd) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
|
|
44
72
|
/**
|
|
45
73
|
* @param {string} desc
|
|
46
74
|
* @param {number} atSignIndex
|
|
@@ -72,8 +100,9 @@ export default iterateJsdoc(({
|
|
|
72
100
|
const atSignIndex = offset + match.lastIndexOf('@');
|
|
73
101
|
const inlineTagName = getInlineTagName(desc, atSignIndex);
|
|
74
102
|
|
|
75
|
-
return
|
|
76
|
-
|
|
103
|
+
return isInsideMarkdownCodeSpan(desc, atSignIndex) ||
|
|
104
|
+
declarationReferenceInlineTags.has(inlineTagName) &&
|
|
105
|
+
scopedPackageNameRegex.test(desc.slice(atSignIndex));
|
|
77
106
|
};
|
|
78
107
|
|
|
79
108
|
/**
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import iterateJsdoc from '../iterateJsdoc.js';
|
|
2
|
+
|
|
3
|
+
const markdownLinkRegex = /(?<!\\)\[([^`\]\r\n]+)\]\(([^`\(\)\s]+)\)/gv;
|
|
4
|
+
const prefixLinkRegex = /(?<!\\)\[([^\]\r\n]+)\]\{@link\s+([^\}\s\|]+)\}/gv;
|
|
5
|
+
const pipeLinkRegex = /\{@link\s+([^\}\s\|]+)\s*\|\s*([^\}\r\n]+)\}/gv;
|
|
6
|
+
|
|
7
|
+
const markdownLinkAttemptRegex = /(?<!\\)\[[^\]\r\n]*\]\([^\r\n]*(?:\)|$)/v;
|
|
8
|
+
const prefixLinkAttemptRegex = /(?<!\\)\[[^\]\r\n]*\]\{@link[^\}\r\n]*(?:\}|$)/v;
|
|
9
|
+
const pipeLinkAttemptRegex = /\{@link[^\}\r\n]*\|[^\}\r\n]*(?:\}|$)/v;
|
|
10
|
+
|
|
11
|
+
const scopedPackageNameRegex = /^@[\w.\-]+\/[\w.\-]+/v;
|
|
12
|
+
const markdownCodeSpanRegex = /(?<!\\)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/gv;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {'pipe'|'prefix'} canonicalForm
|
|
16
|
+
* @param {string} label
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
const cannotSafelyFormatLink = (canonicalForm, label) => {
|
|
20
|
+
if (!label.trim()) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (canonicalForm === 'pipe') {
|
|
25
|
+
return label.includes('}');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return label.includes(']');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} target
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
const encodeLinkTarget = (target) => {
|
|
36
|
+
return encodeURI(target).replaceAll(/%25(?=[\dA-F]{2})/giv, '%');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} description
|
|
41
|
+
* @param {number} index
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
const isInsideMarkdownCodeSpan = (description, index) => {
|
|
45
|
+
for (const match of description.matchAll(markdownCodeSpanRegex)) {
|
|
46
|
+
const delimiterLength = match[1].length;
|
|
47
|
+
const contentStart = match.index + delimiterLength;
|
|
48
|
+
const contentEnd = match.index + match[0].length - delimiterLength;
|
|
49
|
+
|
|
50
|
+
if (index >= contentStart && index < contentEnd) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {'pipe'|'prefix'} canonicalForm
|
|
60
|
+
* @param {string} label
|
|
61
|
+
* @param {string} target
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
const formatLink = (canonicalForm, label, target) => {
|
|
65
|
+
const encodedTarget = encodeLinkTarget(target);
|
|
66
|
+
|
|
67
|
+
return canonicalForm === 'pipe' ?
|
|
68
|
+
`{@link ${encodedTarget}|${label.trim()}}` :
|
|
69
|
+
`[${label.trim()}]{@link ${encodedTarget}}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} description
|
|
74
|
+
* @param {'pipe'|'prefix'} canonicalForm
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
const normalizeDescription = (description, canonicalForm) => {
|
|
78
|
+
const markdownNormalized = description.replaceAll(
|
|
79
|
+
new RegExp(markdownLinkRegex, 'gv'),
|
|
80
|
+
(_match, label, target) => {
|
|
81
|
+
return formatLink(canonicalForm, label, target);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (canonicalForm === 'pipe') {
|
|
86
|
+
return markdownNormalized.replaceAll(
|
|
87
|
+
new RegExp(prefixLinkRegex, 'gv'),
|
|
88
|
+
(_match, label, target) => {
|
|
89
|
+
return formatLink(canonicalForm, label, target);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return markdownNormalized.replaceAll(
|
|
95
|
+
new RegExp(pipeLinkRegex, 'gv'),
|
|
96
|
+
(_match, target, label) => {
|
|
97
|
+
return formatLink(canonicalForm, label, target);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default iterateJsdoc(({
|
|
103
|
+
context,
|
|
104
|
+
jsdoc,
|
|
105
|
+
utils,
|
|
106
|
+
}) => {
|
|
107
|
+
const {
|
|
108
|
+
canonicalForm = 'pipe',
|
|
109
|
+
enableFixer = true,
|
|
110
|
+
} = context.options[0] || {};
|
|
111
|
+
|
|
112
|
+
const descriptionMatcher = canonicalForm === 'pipe' ?
|
|
113
|
+
new RegExp(`${markdownLinkRegex.source}|${prefixLinkRegex.source}`, 'v') :
|
|
114
|
+
new RegExp(`${markdownLinkRegex.source}|${pipeLinkRegex.source}`, 'v');
|
|
115
|
+
|
|
116
|
+
/** @type {import('comment-parser').Spec[]} */
|
|
117
|
+
const fixableTags = [];
|
|
118
|
+
|
|
119
|
+
for (const tag of jsdoc.tags) {
|
|
120
|
+
if (tag.tag !== 'see') {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const firstTokens = tag.source[0].tokens;
|
|
125
|
+
const tagDescription = String(utils.getTagDescription(tag));
|
|
126
|
+
const rawDescription = firstTokens.name ?
|
|
127
|
+
firstTokens.name + firstTokens.postName + tagDescription :
|
|
128
|
+
tagDescription;
|
|
129
|
+
|
|
130
|
+
let hasAmbiguousLink = false;
|
|
131
|
+
let hasNoncanonicalLink = false;
|
|
132
|
+
|
|
133
|
+
for (const match of rawDescription.matchAll(markdownLinkRegex)) {
|
|
134
|
+
const label = match[1];
|
|
135
|
+
const target = match[2];
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
isInsideMarkdownCodeSpan(rawDescription, match.index) ||
|
|
139
|
+
scopedPackageNameRegex.test(target) ||
|
|
140
|
+
cannotSafelyFormatLink(canonicalForm, label)
|
|
141
|
+
) {
|
|
142
|
+
hasAmbiguousLink = true;
|
|
143
|
+
} else {
|
|
144
|
+
hasNoncanonicalLink = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const inlineTag of tag.inlineTags) {
|
|
149
|
+
if (inlineTag.tag !== 'link' || !inlineTag.text) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (inlineTag.format === 'space') {
|
|
154
|
+
hasAmbiguousLink = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cannotSafelyFormatLink(
|
|
159
|
+
canonicalForm,
|
|
160
|
+
inlineTag.text,
|
|
161
|
+
)) {
|
|
162
|
+
hasAmbiguousLink = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (inlineTag.format === canonicalForm) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const {
|
|
171
|
+
start,
|
|
172
|
+
} = /** @type {typeof inlineTag & {start: number}} */ (inlineTag);
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
isInsideMarkdownCodeSpan(rawDescription, start) ||
|
|
176
|
+
scopedPackageNameRegex.test(inlineTag.namepathOrURL)
|
|
177
|
+
) {
|
|
178
|
+
hasAmbiguousLink = true;
|
|
179
|
+
} else {
|
|
180
|
+
hasNoncanonicalLink = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const unrecognizedLinkText = rawDescription
|
|
185
|
+
.replaceAll(new RegExp(markdownLinkRegex, 'gv'), '')
|
|
186
|
+
.replaceAll(new RegExp(prefixLinkRegex, 'gv'), '')
|
|
187
|
+
.replaceAll(new RegExp(pipeLinkRegex, 'gv'), '');
|
|
188
|
+
|
|
189
|
+
hasAmbiguousLink ||= markdownLinkAttemptRegex.test(unrecognizedLinkText) ||
|
|
190
|
+
prefixLinkAttemptRegex.test(unrecognizedLinkText) ||
|
|
191
|
+
pipeLinkAttemptRegex.test(unrecognizedLinkText);
|
|
192
|
+
|
|
193
|
+
if (hasAmbiguousLink) {
|
|
194
|
+
utils.reportJSDoc('@see link cannot be safely normalized.', {
|
|
195
|
+
line: tag.source[0].number,
|
|
196
|
+
}, null);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!hasNoncanonicalLink) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fixableTags.push(tag);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const [
|
|
208
|
+
reportIdx,
|
|
209
|
+
tag,
|
|
210
|
+
] of fixableTags.entries()) {
|
|
211
|
+
utils.reportJSDoc(
|
|
212
|
+
`Expected @see link to use the ${canonicalForm} form.`,
|
|
213
|
+
{
|
|
214
|
+
line: tag.source[0].number,
|
|
215
|
+
},
|
|
216
|
+
enableFixer && reportIdx === 0 ?
|
|
217
|
+
() => {
|
|
218
|
+
for (const fixableTag of fixableTags) {
|
|
219
|
+
const firstTokens = fixableTag.source[0].tokens;
|
|
220
|
+
|
|
221
|
+
if (firstTokens.name) {
|
|
222
|
+
firstTokens.description = firstTokens.name +
|
|
223
|
+
firstTokens.postName + firstTokens.description;
|
|
224
|
+
firstTokens.name = '';
|
|
225
|
+
firstTokens.postName = '';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (
|
|
229
|
+
let sourceIdx = 0;
|
|
230
|
+
sourceIdx < fixableTag.source.length;
|
|
231
|
+
sourceIdx++
|
|
232
|
+
) {
|
|
233
|
+
if (!descriptionMatcher.test(
|
|
234
|
+
fixableTag.source[sourceIdx].tokens.description,
|
|
235
|
+
)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
utils.setTagDescription(
|
|
240
|
+
fixableTag,
|
|
241
|
+
descriptionMatcher,
|
|
242
|
+
(description) => {
|
|
243
|
+
return normalizeDescription(description, canonicalForm);
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} :
|
|
249
|
+
null,
|
|
250
|
+
true,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}, {
|
|
254
|
+
iterateAllJsdocs: true,
|
|
255
|
+
meta: {
|
|
256
|
+
docs: {
|
|
257
|
+
description: 'Normalizes labeled links in `@see` tags to a canonical `{@link}` form.',
|
|
258
|
+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/normalize-see-links.md#repos-sticky-header',
|
|
259
|
+
},
|
|
260
|
+
fixable: 'code',
|
|
261
|
+
schema: [
|
|
262
|
+
{
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
properties: {
|
|
265
|
+
canonicalForm: {
|
|
266
|
+
description: 'The canonical `{@link}` form: `"pipe"` produces `{@link url|label}`, while `"prefix"` produces `[label]{@link url}`. Defaults to `"pipe"`.',
|
|
267
|
+
enum: [
|
|
268
|
+
'pipe',
|
|
269
|
+
'prefix',
|
|
270
|
+
],
|
|
271
|
+
type: 'string',
|
|
272
|
+
},
|
|
273
|
+
enableFixer: {
|
|
274
|
+
description: 'Whether to enable the fixer. Defaults to `true`.',
|
|
275
|
+
type: 'boolean',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
type: 'object',
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
type: 'suggestion',
|
|
282
|
+
},
|
|
283
|
+
});
|
package/src/rules.d.ts
CHANGED
|
@@ -1292,6 +1292,22 @@ export interface Rules {
|
|
|
1292
1292
|
}
|
|
1293
1293
|
];
|
|
1294
1294
|
|
|
1295
|
+
/** Normalizes labeled links in `@see` tags to a canonical `{@link}` form. */
|
|
1296
|
+
"jsdoc/normalize-see-links":
|
|
1297
|
+
| []
|
|
1298
|
+
| [
|
|
1299
|
+
{
|
|
1300
|
+
/**
|
|
1301
|
+
* The canonical `{@link}` form: `"pipe"` produces `{@link url|label}`, while `"prefix"` produces `[label]{@link url}`. Defaults to `"pipe"`.
|
|
1302
|
+
*/
|
|
1303
|
+
canonicalForm?: "pipe" | "prefix";
|
|
1304
|
+
/**
|
|
1305
|
+
* Whether to enable the fixer. Defaults to `true`.
|
|
1306
|
+
*/
|
|
1307
|
+
enableFixer?: boolean;
|
|
1308
|
+
}
|
|
1309
|
+
];
|
|
1310
|
+
|
|
1295
1311
|
/** Prefer `@import` tags to inline `import()` statements. */
|
|
1296
1312
|
"jsdoc/prefer-import-tag":
|
|
1297
1313
|
| []
|