@kazupon/eslint-plugin 0.4.0 → 0.6.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 +6 -5
- package/lib/index.d.ts +9 -0
- package/lib/index.js +438 -139
- package/package.json +33 -31
package/README.md
CHANGED
|
@@ -61,11 +61,12 @@ The rules with the following star ⭐ are included in the configs.
|
|
|
61
61
|
|
|
62
62
|
### @kazupon/eslint-plugin Rules
|
|
63
63
|
|
|
64
|
-
| Rule ID
|
|
65
|
-
|
|
|
66
|
-
| [@kazupon/enforce-header-comment](https://eslint-plugin.kazupon.dev/rules/enforce-header-comment.html)
|
|
67
|
-
| [@kazupon/no-tag-comments](https://eslint-plugin.kazupon.dev/rules/no-tag-comments.html)
|
|
68
|
-
| [@kazupon/prefer-
|
|
64
|
+
| Rule ID | Description | Category | Fixable | RECOMMENDED |
|
|
65
|
+
| :--------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :------- | :-----: | :---------: |
|
|
66
|
+
| [@kazupon/enforce-header-comment](https://eslint-plugin.kazupon.dev/rules/enforce-header-comment.html) | Enforce heading the comment in source code file | Comment | | ⭐ |
|
|
67
|
+
| [@kazupon/no-tag-comments](https://eslint-plugin.kazupon.dev/rules/no-tag-comments.html) | disallow tag comments | Comment | | ⭐ |
|
|
68
|
+
| [@kazupon/prefer-inline-code-words-comments](https://eslint-plugin.kazupon.dev/rules/prefer-inline-code-words-comments.html) | enforce the use of inline code for specific words on comments | Comment | 🔧 | ⭐ |
|
|
69
|
+
| [@kazupon/prefer-scope-on-tag-comment](https://eslint-plugin.kazupon.dev/rules/prefer-scope-on-tag-comment.html) | enforce adding a scope to tag comments | Comment | | ⭐ |
|
|
69
70
|
|
|
70
71
|
<!--RULES_TABLE_END-->
|
|
71
72
|
|
package/lib/index.d.ts
CHANGED
|
@@ -10,8 +10,17 @@ declare const plugin: Omit<ESLint.Plugin, "configs"> & {
|
|
|
10
10
|
};
|
|
11
11
|
declare const recommendedConfig: Linter.Config[];
|
|
12
12
|
declare const commentConfig: Linter.Config[];
|
|
13
|
+
/**
|
|
14
|
+
* Plugin Configurations.
|
|
15
|
+
*/
|
|
13
16
|
declare const configs: {
|
|
17
|
+
/**
|
|
18
|
+
* Recommended configuration
|
|
19
|
+
*/
|
|
14
20
|
recommended: typeof recommendedConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Comment configuration
|
|
23
|
+
*/
|
|
15
24
|
comment: typeof commentConfig;
|
|
16
25
|
};
|
|
17
26
|
/** @alias */
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,194 @@
|
|
|
1
1
|
import { parseComment } from "@es-joy/jsdoccomment";
|
|
2
2
|
|
|
3
|
+
//#region src/utils/comment.ts
|
|
4
|
+
/**
|
|
5
|
+
* Remove JSDoc asterisk prefix if present
|
|
6
|
+
*
|
|
7
|
+
* @param line - The line of text to strip
|
|
8
|
+
* @returns The stripped line of text
|
|
9
|
+
*/
|
|
10
|
+
function stripJSDocPrefix(line) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
return trimmed.startsWith("*") ? trimmed.slice(1).trim() : trimmed;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if the text starts with any of the given tags
|
|
16
|
+
*
|
|
17
|
+
* @param text - The text to check
|
|
18
|
+
* @param tags - Array of tags to search for
|
|
19
|
+
* @returns Tag detection result or null if no tag found
|
|
20
|
+
*/
|
|
21
|
+
function detectTag(text, tags) {
|
|
22
|
+
for (const tag of tags) {
|
|
23
|
+
const tagRegex = /* @__PURE__ */ new RegExp(`^${tag}\\b`);
|
|
24
|
+
const match = text.match(tagRegex);
|
|
25
|
+
if (match) {
|
|
26
|
+
const afterTag = text.slice(tag.length);
|
|
27
|
+
if (afterTag.startsWith("(")) {
|
|
28
|
+
const closingParenIndex = afterTag.indexOf(")");
|
|
29
|
+
if (closingParenIndex > 0) {
|
|
30
|
+
const scope = afterTag.slice(1, closingParenIndex).trim();
|
|
31
|
+
return {
|
|
32
|
+
tag,
|
|
33
|
+
hasScope: scope.length > 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
tag,
|
|
38
|
+
hasScope: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (afterTag === "" || afterTag.startsWith(":") || afterTag.startsWith(" ")) return {
|
|
42
|
+
tag,
|
|
43
|
+
hasScope: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate the exact location of a tag in a comment
|
|
51
|
+
*
|
|
52
|
+
* @param comment - The comment containing the tag
|
|
53
|
+
* @param line - The line of text containing the tag
|
|
54
|
+
* @param lineIndex - The index of the line within the comment
|
|
55
|
+
* @param tag - The tag to locate
|
|
56
|
+
* @returns Location with line and column, or null if not found
|
|
57
|
+
*/
|
|
58
|
+
function calculateTagLocation(comment, line, lineIndex, tag) {
|
|
59
|
+
const tagIndex = line.indexOf(tag);
|
|
60
|
+
if (tagIndex === -1) return null;
|
|
61
|
+
if (lineIndex === 0) {
|
|
62
|
+
const tagIndexInValue = comment.value.indexOf(tag);
|
|
63
|
+
return tagIndexInValue === -1 ? null : {
|
|
64
|
+
line: comment.loc.start.line,
|
|
65
|
+
column: comment.loc.start.column + 2 + tagIndexInValue
|
|
66
|
+
};
|
|
67
|
+
} else return {
|
|
68
|
+
line: comment.loc.start.line + lineIndex,
|
|
69
|
+
column: tagIndex
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Process all comments in a source file
|
|
74
|
+
*
|
|
75
|
+
* @param sourceCode - The ESLint source code object
|
|
76
|
+
* @param callback - Function to call for each comment
|
|
77
|
+
*/
|
|
78
|
+
function processAllComments(sourceCode, callback) {
|
|
79
|
+
const comments = sourceCode.getAllComments();
|
|
80
|
+
for (const comment of comments) callback(comment);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Calculate the position of a word in a comment
|
|
84
|
+
*
|
|
85
|
+
* @param comment - The comment containing the word
|
|
86
|
+
* @param wordIndex - The index of the word in the comment value
|
|
87
|
+
* @param _word - The word itself (unused but needed for API compatibility)
|
|
88
|
+
* @returns Location with line and column
|
|
89
|
+
*/
|
|
90
|
+
function calculateWordPosition(comment, wordIndex, _word) {
|
|
91
|
+
const { value, type } = comment;
|
|
92
|
+
if (type === "Line") return {
|
|
93
|
+
line: comment.loc.start.line,
|
|
94
|
+
column: comment.loc.start.column + 2 + wordIndex
|
|
95
|
+
};
|
|
96
|
+
const beforeMatch = value.slice(0, Math.max(0, wordIndex));
|
|
97
|
+
const beforeLines = beforeMatch.split("\n");
|
|
98
|
+
const lineIndex = beforeLines.length - 1;
|
|
99
|
+
const line = comment.loc.start.line + lineIndex;
|
|
100
|
+
if (lineIndex === 0) return {
|
|
101
|
+
line,
|
|
102
|
+
column: comment.loc.start.column + 2 + beforeMatch.length
|
|
103
|
+
};
|
|
104
|
+
else {
|
|
105
|
+
const columnInValue = beforeMatch.length - beforeLines.slice(0, -1).join("\n").length - 1;
|
|
106
|
+
return {
|
|
107
|
+
line,
|
|
108
|
+
column: Math.max(0, columnInValue)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Report a comment violation with standardized location handling
|
|
114
|
+
*
|
|
115
|
+
* @param ctx - The ESLint rule context
|
|
116
|
+
* @param comment - The comment where the violation occurred
|
|
117
|
+
* @param messageId - The message ID to report
|
|
118
|
+
* @param data - Data for the message template
|
|
119
|
+
* @param location - Optional specific location within the comment
|
|
120
|
+
*/
|
|
121
|
+
function reportCommentViolation(ctx, comment, messageId, data, location) {
|
|
122
|
+
if (!comment.loc) {
|
|
123
|
+
ctx.report({
|
|
124
|
+
messageId,
|
|
125
|
+
data,
|
|
126
|
+
node: ctx.sourceCode.ast
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (location) ctx.report({
|
|
131
|
+
messageId,
|
|
132
|
+
data,
|
|
133
|
+
loc: {
|
|
134
|
+
start: {
|
|
135
|
+
line: location.line,
|
|
136
|
+
column: location.column
|
|
137
|
+
},
|
|
138
|
+
end: {
|
|
139
|
+
line: location.line,
|
|
140
|
+
column: location.column + (location.length || 0)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
else ctx.report({
|
|
145
|
+
messageId,
|
|
146
|
+
data,
|
|
147
|
+
loc: comment.loc
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse directive comment and extract description
|
|
152
|
+
* Handles both eslint-style (--) and TypeScript-style (space) separators
|
|
153
|
+
*
|
|
154
|
+
* @param text - The comment text to parse
|
|
155
|
+
* @param directives - List of directive patterns to check for
|
|
156
|
+
* @returns Parsed directive info or null if not a directive
|
|
157
|
+
*/
|
|
158
|
+
function parseDirectiveComment(text, directives) {
|
|
159
|
+
const trimmedText = text.trim();
|
|
160
|
+
for (const directive of directives) if (trimmedText.startsWith(directive)) {
|
|
161
|
+
const afterDirective = trimmedText.slice(directive.length);
|
|
162
|
+
const separatorIndex = afterDirective.indexOf("--");
|
|
163
|
+
if (separatorIndex !== -1) {
|
|
164
|
+
const description = afterDirective.slice(separatorIndex + 2).trim();
|
|
165
|
+
const separatorPos = text.indexOf("--");
|
|
166
|
+
const afterSeparator = text.slice(separatorPos + 2);
|
|
167
|
+
const descriptionStart = separatorPos + 2 + (afterSeparator.length - afterSeparator.trimStart().length);
|
|
168
|
+
return {
|
|
169
|
+
directive,
|
|
170
|
+
description,
|
|
171
|
+
descriptionStart
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (afterDirective.trim()) {
|
|
175
|
+
const spaceMatch = afterDirective.match(/^\s+/);
|
|
176
|
+
if (spaceMatch) {
|
|
177
|
+
const description = afterDirective.trim();
|
|
178
|
+
const directiveIndex = text.indexOf(directive);
|
|
179
|
+
const descriptionStart = directiveIndex + directive.length + spaceMatch[0].length;
|
|
180
|
+
return {
|
|
181
|
+
directive,
|
|
182
|
+
description,
|
|
183
|
+
descriptionStart
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
3
192
|
//#region src/utils/constants.ts
|
|
4
193
|
/**
|
|
5
194
|
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
@@ -12,7 +201,7 @@ const name = "@kazupon/eslint-plugin";
|
|
|
12
201
|
/**
|
|
13
202
|
* The plugin version.
|
|
14
203
|
*/
|
|
15
|
-
const version = "0.
|
|
204
|
+
const version = "0.6.0";
|
|
16
205
|
/**
|
|
17
206
|
* The namespace for rules
|
|
18
207
|
*/
|
|
@@ -22,7 +211,7 @@ const namespace = "@kazupon";
|
|
|
22
211
|
//#region src/utils/rule.ts
|
|
23
212
|
const BLOB_URL = "https://eslint-plugin.kazupon.dev/rules";
|
|
24
213
|
function RuleCreator(urlCreator, namespace$1 = "") {
|
|
25
|
-
return function createNamedRule({ meta, name: name$1,...rule$
|
|
214
|
+
return function createNamedRule({ meta, name: name$1,...rule$4 }) {
|
|
26
215
|
const ruleId = namespace$1 ? `${namespace$1}/${name$1}` : name$1;
|
|
27
216
|
return {
|
|
28
217
|
meta: {
|
|
@@ -34,7 +223,7 @@ function RuleCreator(urlCreator, namespace$1 = "") {
|
|
|
34
223
|
ruleId
|
|
35
224
|
}
|
|
36
225
|
},
|
|
37
|
-
...rule$
|
|
226
|
+
...rule$4
|
|
38
227
|
};
|
|
39
228
|
};
|
|
40
229
|
}
|
|
@@ -53,7 +242,7 @@ function initializeTagDiagnosis(tags) {
|
|
|
53
242
|
function validTagDiagnosis(tagDiagnosis) {
|
|
54
243
|
return Object.keys(tagDiagnosis).every((tag) => tagDiagnosis[tag] === "ok");
|
|
55
244
|
}
|
|
56
|
-
const rule$
|
|
245
|
+
const rule$3 = createRule({
|
|
57
246
|
name: "enforce-header-comment",
|
|
58
247
|
meta: {
|
|
59
248
|
type: "suggestion",
|
|
@@ -74,6 +263,7 @@ const rule$2 = createRule({
|
|
|
74
263
|
create(ctx) {
|
|
75
264
|
/**
|
|
76
265
|
* Report the tag diagnosis
|
|
266
|
+
*
|
|
77
267
|
* @param comment - A target comment node
|
|
78
268
|
* @param tags - A list of tags to check
|
|
79
269
|
* @param tagDiagnosis - A map of tag diagnosis
|
|
@@ -83,11 +273,7 @@ const rule$2 = createRule({
|
|
|
83
273
|
let reported = false;
|
|
84
274
|
for (const tag of tags) {
|
|
85
275
|
if (tagDiagnosis[tag] === "ok") continue;
|
|
86
|
-
ctx
|
|
87
|
-
loc: comment.loc,
|
|
88
|
-
messageId: tagDiagnosis[tag] === "require" ? "headerCommentNeedTag" : "headerCommentNeedTagValue",
|
|
89
|
-
data: { tag }
|
|
90
|
-
});
|
|
276
|
+
reportCommentViolation(ctx, comment, tagDiagnosis[tag] === "require" ? "headerCommentNeedTag" : "headerCommentNeedTagValue", { tag });
|
|
91
277
|
reported = true;
|
|
92
278
|
}
|
|
93
279
|
return reported;
|
|
@@ -181,78 +367,33 @@ const rule$2 = createRule({
|
|
|
181
367
|
};
|
|
182
368
|
}
|
|
183
369
|
});
|
|
184
|
-
var enforce_header_comment_default = rule$
|
|
370
|
+
var enforce_header_comment_default = rule$3;
|
|
185
371
|
|
|
186
372
|
//#endregion
|
|
187
|
-
//#region src/utils/
|
|
188
|
-
/**
|
|
189
|
-
* Remove JSDoc asterisk prefix if present
|
|
190
|
-
*/
|
|
191
|
-
function stripJSDocPrefix(line) {
|
|
192
|
-
const trimmed = line.trim();
|
|
193
|
-
return trimmed.startsWith("*") ? trimmed.slice(1).trim() : trimmed;
|
|
194
|
-
}
|
|
373
|
+
//#region src/utils/options.ts
|
|
195
374
|
/**
|
|
196
|
-
*
|
|
197
|
-
* @
|
|
198
|
-
* @param tags Array of tags to search for
|
|
199
|
-
* @returns Tag detection result or null if no tag found
|
|
375
|
+
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
376
|
+
* @license MIT
|
|
200
377
|
*/
|
|
201
|
-
function detectTag(text, tags) {
|
|
202
|
-
for (const tag of tags) {
|
|
203
|
-
const tagRegex = new RegExp(`^${tag}\\b`);
|
|
204
|
-
const match = text.match(tagRegex);
|
|
205
|
-
if (match) {
|
|
206
|
-
const afterTag = text.slice(tag.length);
|
|
207
|
-
if (afterTag.startsWith("(")) {
|
|
208
|
-
const closingParenIndex = afterTag.indexOf(")");
|
|
209
|
-
if (closingParenIndex > 0) {
|
|
210
|
-
const scope = afterTag.slice(1, closingParenIndex).trim();
|
|
211
|
-
return {
|
|
212
|
-
tag,
|
|
213
|
-
hasScope: scope.length > 0
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
tag,
|
|
218
|
-
hasScope: false
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
if (afterTag === "" || afterTag.startsWith(":") || afterTag.startsWith(" ")) return {
|
|
222
|
-
tag,
|
|
223
|
-
hasScope: false
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
378
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* @
|
|
233
|
-
*
|
|
234
|
-
* @param
|
|
235
|
-
* @
|
|
379
|
+
* Parse array options with defaults
|
|
380
|
+
*
|
|
381
|
+
* @typeParam T - The type of the options object
|
|
382
|
+
*
|
|
383
|
+
* @param options - The options object that may contain array fields
|
|
384
|
+
* @param arrayFields - Object mapping field names to their default arrays
|
|
385
|
+
* @returns The parsed options with default arrays applied
|
|
236
386
|
*/
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
const tagIndexInValue = comment.value.indexOf(tag);
|
|
242
|
-
return tagIndexInValue === -1 ? null : {
|
|
243
|
-
line: comment.loc.start.line,
|
|
244
|
-
column: comment.loc.start.column + 2 + tagIndexInValue
|
|
245
|
-
};
|
|
246
|
-
} else return {
|
|
247
|
-
line: comment.loc.start.line + lineIndex,
|
|
248
|
-
column: tagIndex
|
|
249
|
-
};
|
|
387
|
+
function parseArrayOptions(options, arrayFields) {
|
|
388
|
+
const result = options ? { ...options } : {};
|
|
389
|
+
for (const [field, defaultArray] of Object.entries(arrayFields)) if (!result[field] || !Array.isArray(result[field])) result[field] = defaultArray;
|
|
390
|
+
return result;
|
|
250
391
|
}
|
|
251
392
|
|
|
252
393
|
//#endregion
|
|
253
394
|
//#region src/rules/no-tag-comments.ts
|
|
254
395
|
const DEFAULT_TAGS$1 = ["FIXME", "BUG"];
|
|
255
|
-
const rule$
|
|
396
|
+
const rule$2 = createRule({
|
|
256
397
|
name: "no-tag-comments",
|
|
257
398
|
meta: {
|
|
258
399
|
type: "problem",
|
|
@@ -275,35 +416,26 @@ const rule$1 = createRule({
|
|
|
275
416
|
}]
|
|
276
417
|
},
|
|
277
418
|
create(ctx) {
|
|
278
|
-
const options = ctx.options[0]
|
|
279
|
-
const tags = options.tags
|
|
419
|
+
const options = parseArrayOptions(ctx.options[0], { tags: DEFAULT_TAGS$1 });
|
|
420
|
+
const tags = options.tags;
|
|
280
421
|
const sourceCode = ctx.sourceCode;
|
|
281
422
|
/**
|
|
282
423
|
* Report a tag comment violation
|
|
424
|
+
*
|
|
425
|
+
* @param comment - The comment node to report
|
|
426
|
+
* @param tag - The tag that was found in the comment
|
|
427
|
+
* @param loc - Optional location information for the tag
|
|
283
428
|
*/
|
|
284
429
|
function reportTag(comment, tag, loc) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
start: {
|
|
290
|
-
line: loc.line,
|
|
291
|
-
column: loc.column
|
|
292
|
-
},
|
|
293
|
-
end: {
|
|
294
|
-
line: loc.line,
|
|
295
|
-
column: loc.column + tag.length
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
else ctx.report({
|
|
300
|
-
messageId: "tagComment",
|
|
301
|
-
data: { tag },
|
|
302
|
-
loc: comment.loc
|
|
303
|
-
});
|
|
430
|
+
reportCommentViolation(ctx, comment, "tagComment", { tag }, loc ? {
|
|
431
|
+
...loc,
|
|
432
|
+
length: tag.length
|
|
433
|
+
} : void 0);
|
|
304
434
|
}
|
|
305
435
|
/**
|
|
306
436
|
* Check a comment for tag violations
|
|
437
|
+
*
|
|
438
|
+
* @param comment - The comment node to check
|
|
307
439
|
*/
|
|
308
440
|
function checkComment(comment) {
|
|
309
441
|
const { value, type } = comment;
|
|
@@ -334,12 +466,124 @@ const rule$1 = createRule({
|
|
|
334
466
|
}
|
|
335
467
|
}
|
|
336
468
|
return { Program() {
|
|
337
|
-
|
|
338
|
-
|
|
469
|
+
processAllComments(sourceCode, checkComment);
|
|
470
|
+
} };
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
var no_tag_comments_default = rule$2;
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/utils/regex.ts
|
|
477
|
+
/**
|
|
478
|
+
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
479
|
+
* @license MIT
|
|
480
|
+
*/
|
|
481
|
+
/**
|
|
482
|
+
* Escape special regex characters in a string
|
|
483
|
+
*
|
|
484
|
+
* @param str - The string to escape
|
|
485
|
+
* @returns The escaped string
|
|
486
|
+
*/
|
|
487
|
+
function escapeRegExp(str) {
|
|
488
|
+
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create a regex pattern with word boundaries
|
|
492
|
+
*
|
|
493
|
+
* @param word - The word to match
|
|
494
|
+
* @param flags - Optional regex flags
|
|
495
|
+
* @returns A RegExp that matches the word with boundaries
|
|
496
|
+
*/
|
|
497
|
+
function createWordBoundaryRegex(word, flags = "g") {
|
|
498
|
+
const escapedWord = escapeRegExp(word);
|
|
499
|
+
return new RegExp(`\\b${escapedWord}\\b`, flags);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Check if a word at a given index is wrapped with specific delimiters
|
|
503
|
+
*
|
|
504
|
+
* @param text - The text to check
|
|
505
|
+
* @param index - The starting index of the word
|
|
506
|
+
* @param word - The word to check
|
|
507
|
+
* @param startDelimiter - The starting delimiter (default: '`')
|
|
508
|
+
* @param endDelimiter - The ending delimiter (default: '`')
|
|
509
|
+
* @returns True if the word is wrapped with the delimiters
|
|
510
|
+
*/
|
|
511
|
+
function isWordWrapped(text, index, word, startDelimiter = "`", endDelimiter = "`") {
|
|
512
|
+
const beforeIndex = index - 1;
|
|
513
|
+
const afterIndex = index + word.length;
|
|
514
|
+
return beforeIndex >= 0 && afterIndex < text.length && text[beforeIndex] === startDelimiter && text[afterIndex] === endDelimiter;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/rules/prefer-inline-code-words-comments.ts
|
|
519
|
+
const rule$1 = createRule({
|
|
520
|
+
name: "prefer-inline-code-words-comments",
|
|
521
|
+
meta: {
|
|
522
|
+
type: "suggestion",
|
|
523
|
+
docs: {
|
|
524
|
+
description: "enforce the use of inline code for specific words on comments",
|
|
525
|
+
category: "Comment",
|
|
526
|
+
recommended: true,
|
|
527
|
+
defaultSeverity: "error"
|
|
528
|
+
},
|
|
529
|
+
fixable: "code",
|
|
530
|
+
messages: { missingInlineCode: "The word \"{{word}}\" should be wrapped in inline code" },
|
|
531
|
+
schema: [{
|
|
532
|
+
type: "object",
|
|
533
|
+
properties: { words: {
|
|
534
|
+
type: "array",
|
|
535
|
+
items: { type: "string" },
|
|
536
|
+
minItems: 1,
|
|
537
|
+
uniqueItems: true
|
|
538
|
+
} },
|
|
539
|
+
required: ["words"],
|
|
540
|
+
additionalProperties: false
|
|
541
|
+
}]
|
|
542
|
+
},
|
|
543
|
+
create(ctx) {
|
|
544
|
+
const options = ctx.options[0];
|
|
545
|
+
if (!options || !options.words || options.words.length === 0) return {};
|
|
546
|
+
const words = options.words;
|
|
547
|
+
const sourceCode = ctx.sourceCode;
|
|
548
|
+
/**
|
|
549
|
+
* Check a comment for words that should be wrapped in inline code
|
|
550
|
+
*
|
|
551
|
+
* @param comment - The comment node to check
|
|
552
|
+
*/
|
|
553
|
+
function checkComment(comment) {
|
|
554
|
+
const { value } = comment;
|
|
555
|
+
for (const word of words) {
|
|
556
|
+
const regex = createWordBoundaryRegex(word);
|
|
557
|
+
let match;
|
|
558
|
+
while ((match = regex.exec(value)) !== null) {
|
|
559
|
+
const index = match.index;
|
|
560
|
+
if (isWordWrapped(value, index, word)) continue;
|
|
561
|
+
const position = calculateWordPosition(comment, index, word);
|
|
562
|
+
ctx.report({
|
|
563
|
+
messageId: "missingInlineCode",
|
|
564
|
+
data: { word },
|
|
565
|
+
loc: {
|
|
566
|
+
start: position,
|
|
567
|
+
end: {
|
|
568
|
+
line: position.line,
|
|
569
|
+
column: position.column + word.length
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
fix(fixer) {
|
|
573
|
+
const startOffset = comment.range[0] + 2 + index;
|
|
574
|
+
const endOffset = startOffset + word.length;
|
|
575
|
+
return fixer.replaceTextRange([startOffset, endOffset], `\`${word}\``);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { Program() {
|
|
582
|
+
processAllComments(sourceCode, checkComment);
|
|
339
583
|
} };
|
|
340
584
|
}
|
|
341
585
|
});
|
|
342
|
-
var
|
|
586
|
+
var prefer_inline_code_words_comments_default = rule$1;
|
|
343
587
|
|
|
344
588
|
//#endregion
|
|
345
589
|
//#region src/rules/prefer-scope-on-tag-comment.ts
|
|
@@ -350,6 +594,14 @@ const DEFAULT_TAGS = [
|
|
|
350
594
|
"BUG",
|
|
351
595
|
"NOTE"
|
|
352
596
|
];
|
|
597
|
+
const DEFAULT_DIRECTIVES = [
|
|
598
|
+
"eslint-disable",
|
|
599
|
+
"eslint-disable-next-line",
|
|
600
|
+
"eslint-disable-line",
|
|
601
|
+
"@ts-expect-error",
|
|
602
|
+
"@ts-ignore",
|
|
603
|
+
"@ts-nocheck"
|
|
604
|
+
];
|
|
353
605
|
const rule = createRule({
|
|
354
606
|
name: "prefer-scope-on-tag-comment",
|
|
355
607
|
meta: {
|
|
@@ -363,57 +615,63 @@ const rule = createRule({
|
|
|
363
615
|
messages: { missingScope: "Tag comment '{{tag}}' is missing a scope. Use format: {{tag}}(scope)" },
|
|
364
616
|
schema: [{
|
|
365
617
|
type: "object",
|
|
366
|
-
properties: {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
618
|
+
properties: {
|
|
619
|
+
tags: {
|
|
620
|
+
type: "array",
|
|
621
|
+
items: { type: "string" },
|
|
622
|
+
minItems: 1,
|
|
623
|
+
uniqueItems: true
|
|
624
|
+
},
|
|
625
|
+
directives: {
|
|
626
|
+
type: "array",
|
|
627
|
+
items: { type: "string" },
|
|
628
|
+
minItems: 1,
|
|
629
|
+
uniqueItems: true
|
|
630
|
+
}
|
|
631
|
+
},
|
|
372
632
|
additionalProperties: false
|
|
373
633
|
}]
|
|
374
634
|
},
|
|
375
635
|
create(ctx) {
|
|
376
|
-
const options = ctx.options[0]
|
|
377
|
-
|
|
636
|
+
const options = parseArrayOptions(ctx.options[0], {
|
|
637
|
+
tags: DEFAULT_TAGS,
|
|
638
|
+
directives: DEFAULT_DIRECTIVES
|
|
639
|
+
});
|
|
640
|
+
const tags = options.tags;
|
|
641
|
+
const directives = options.directives;
|
|
378
642
|
const sourceCode = ctx.sourceCode;
|
|
379
643
|
/**
|
|
380
644
|
* Report a missing scope violation
|
|
645
|
+
*
|
|
646
|
+
* @param comment - The comment node to report
|
|
647
|
+
* @param tag - The tag that is missing a scope
|
|
381
648
|
*/
|
|
382
649
|
function reportMissingScope(comment, tag, loc) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
node: ctx.sourceCode.ast
|
|
388
|
-
});
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (loc && comment.loc) ctx.report({
|
|
392
|
-
messageId: "missingScope",
|
|
393
|
-
data: { tag },
|
|
394
|
-
loc: {
|
|
395
|
-
start: {
|
|
396
|
-
line: loc.line,
|
|
397
|
-
column: loc.column
|
|
398
|
-
},
|
|
399
|
-
end: {
|
|
400
|
-
line: loc.line,
|
|
401
|
-
column: loc.column + tag.length
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
else ctx.report({
|
|
406
|
-
messageId: "missingScope",
|
|
407
|
-
data: { tag },
|
|
408
|
-
loc: comment.loc
|
|
409
|
-
});
|
|
650
|
+
reportCommentViolation(ctx, comment, "missingScope", { tag }, loc ? {
|
|
651
|
+
...loc,
|
|
652
|
+
length: tag.length
|
|
653
|
+
} : void 0);
|
|
410
654
|
}
|
|
411
655
|
/**
|
|
412
656
|
* Check a comment for missing scope
|
|
657
|
+
*
|
|
658
|
+
* @param comment - The comment node to check
|
|
413
659
|
*/
|
|
414
660
|
function checkComment(comment) {
|
|
415
661
|
const { value, type } = comment;
|
|
416
662
|
if (type === "Line") {
|
|
663
|
+
const directiveInfo$1 = parseDirectiveComment(value, directives);
|
|
664
|
+
if (directiveInfo$1) {
|
|
665
|
+
const tagInfo$1 = detectTag(directiveInfo$1.description, tags);
|
|
666
|
+
if (tagInfo$1 && !tagInfo$1.hasScope) {
|
|
667
|
+
const tagIndex = directiveInfo$1.description.indexOf(tagInfo$1.tag);
|
|
668
|
+
if (tagIndex !== -1) reportMissingScope(comment, tagInfo$1.tag, {
|
|
669
|
+
line: comment.loc.start.line,
|
|
670
|
+
column: comment.loc.start.column + 2 + directiveInfo$1.descriptionStart + tagIndex
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
417
675
|
const tagInfo = detectTag(value.trim(), tags);
|
|
418
676
|
if (tagInfo && !tagInfo.hasScope) {
|
|
419
677
|
const tagIndex = value.indexOf(tagInfo.tag);
|
|
@@ -425,6 +683,44 @@ const rule = createRule({
|
|
|
425
683
|
return;
|
|
426
684
|
}
|
|
427
685
|
const lines = value.split("\n");
|
|
686
|
+
const directiveInfo = parseDirectiveComment(value, directives);
|
|
687
|
+
if (directiveInfo) {
|
|
688
|
+
const tagInfo = detectTag(directiveInfo.description, tags);
|
|
689
|
+
if (tagInfo && !tagInfo.hasScope) if (lines.length === 1) {
|
|
690
|
+
const tagIndexInDesc = directiveInfo.description.indexOf(tagInfo.tag);
|
|
691
|
+
if (tagIndexInDesc !== -1) {
|
|
692
|
+
const tagIndexInValue = directiveInfo.descriptionStart + tagIndexInDesc;
|
|
693
|
+
const location = {
|
|
694
|
+
line: comment.loc.start.line,
|
|
695
|
+
column: comment.loc.start.column + 2 + tagIndexInValue
|
|
696
|
+
};
|
|
697
|
+
reportMissingScope(comment, tagInfo.tag, location);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
const descLines = directiveInfo.description.split("\n");
|
|
701
|
+
for (const [descLineIndex, descLine] of descLines.entries()) {
|
|
702
|
+
const lineTagInfo = detectTag(descLine.trim(), tags);
|
|
703
|
+
if (lineTagInfo && !lineTagInfo.hasScope) {
|
|
704
|
+
const descStartInComment = value.indexOf(directiveInfo.description);
|
|
705
|
+
const linesBeforeDesc = value.slice(0, descStartInComment).split("\n").length - 1;
|
|
706
|
+
const actualLineIndex = linesBeforeDesc + descLineIndex;
|
|
707
|
+
if (actualLineIndex < lines.length) {
|
|
708
|
+
const actualLine = lines[actualLineIndex];
|
|
709
|
+
const tagIndex = actualLine.indexOf(lineTagInfo.tag);
|
|
710
|
+
if (tagIndex !== -1) {
|
|
711
|
+
const location = {
|
|
712
|
+
line: comment.loc.start.line + actualLineIndex,
|
|
713
|
+
column: actualLineIndex === 0 ? comment.loc.start.column + 2 + tagIndex : tagIndex
|
|
714
|
+
};
|
|
715
|
+
reportMissingScope(comment, lineTagInfo.tag, location);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
428
724
|
for (const [i, line] of lines.entries()) {
|
|
429
725
|
const trimmedLine = line.trim();
|
|
430
726
|
if (!trimmedLine) continue;
|
|
@@ -440,8 +736,7 @@ const rule = createRule({
|
|
|
440
736
|
}
|
|
441
737
|
}
|
|
442
738
|
return { Program() {
|
|
443
|
-
|
|
444
|
-
for (const comment of comments) checkComment(comment);
|
|
739
|
+
processAllComments(sourceCode, checkComment);
|
|
445
740
|
} };
|
|
446
741
|
}
|
|
447
742
|
});
|
|
@@ -452,7 +747,8 @@ var prefer_scope_on_tag_comment_default = rule;
|
|
|
452
747
|
const rules = {
|
|
453
748
|
"enforce-header-comment": enforce_header_comment_default,
|
|
454
749
|
"no-tag-comments": no_tag_comments_default,
|
|
455
|
-
"prefer-scope-on-tag-comment": prefer_scope_on_tag_comment_default
|
|
750
|
+
"prefer-scope-on-tag-comment": prefer_scope_on_tag_comment_default,
|
|
751
|
+
"prefer-inline-code-words-comments": prefer_inline_code_words_comments_default
|
|
456
752
|
};
|
|
457
753
|
|
|
458
754
|
//#endregion
|
|
@@ -474,10 +770,10 @@ const recommendedConfig = [{
|
|
|
474
770
|
"**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
|
|
475
771
|
],
|
|
476
772
|
plugins: { [namespace]: plugin },
|
|
477
|
-
rules: Object.entries(rules).reduce((acc, [ruleName, rule$
|
|
478
|
-
if (rule$
|
|
479
|
-
const ruleId = rule$
|
|
480
|
-
acc[ruleId] = rule$
|
|
773
|
+
rules: Object.entries(rules).reduce((acc, [ruleName, rule$4]) => {
|
|
774
|
+
if (rule$4.meta?.docs?.recommended) {
|
|
775
|
+
const ruleId = rule$4.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
|
|
776
|
+
acc[ruleId] = rule$4.meta?.docs?.defaultSeverity || "warn";
|
|
481
777
|
}
|
|
482
778
|
return acc;
|
|
483
779
|
}, Object.create(null))
|
|
@@ -491,12 +787,15 @@ const commentConfig = [{
|
|
|
491
787
|
"**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
|
|
492
788
|
],
|
|
493
789
|
plugins: { [namespace]: plugin },
|
|
494
|
-
rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$
|
|
495
|
-
const ruleId = rule$
|
|
496
|
-
rules$1[ruleId] = rule$
|
|
790
|
+
rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$4]) => {
|
|
791
|
+
const ruleId = rule$4.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
|
|
792
|
+
rules$1[ruleId] = rule$4.meta?.docs?.defaultSeverity || "warn";
|
|
497
793
|
return rules$1;
|
|
498
794
|
}, Object.create(null))
|
|
499
795
|
}];
|
|
796
|
+
/**
|
|
797
|
+
* Plugin Configurations.
|
|
798
|
+
*/
|
|
500
799
|
const configs = {
|
|
501
800
|
recommended: recommendedConfig,
|
|
502
801
|
comment: commentConfig
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kazupon/eslint-plugin",
|
|
3
3
|
"description": "ESLint plugin for @kazupon",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"funding": "https://github.com/sponsors/kazupon",
|
|
7
7
|
"bugs": {
|
|
@@ -47,48 +47,50 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@es-joy/jsdoccomment": "^0.
|
|
51
|
-
"@eslint/core": "^0.
|
|
50
|
+
"@es-joy/jsdoccomment": "^0.52.0",
|
|
51
|
+
"@eslint/core": "^0.15.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"eslint": "^9.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@eslint/compat": "^1.
|
|
58
|
-
"@eslint/markdown": "^
|
|
59
|
-
"@kazupon/eslint-config": "^0.
|
|
57
|
+
"@eslint/compat": "^1.3.1",
|
|
58
|
+
"@eslint/markdown": "^7.1.0",
|
|
59
|
+
"@kazupon/eslint-config": "^0.34.0",
|
|
60
60
|
"@kazupon/prettier-config": "^0.1.1",
|
|
61
|
-
"@shikijs/vitepress-twoslash": "^3.
|
|
62
|
-
"@types/node": "^22.
|
|
63
|
-
"@vitest/eslint-plugin": "^1.
|
|
64
|
-
"bumpp": "^10.
|
|
65
|
-
"eslint": "^9.
|
|
66
|
-
"eslint-config-prettier": "^10.1.
|
|
67
|
-
"eslint-import-resolver-typescript": "^4.4.
|
|
68
|
-
"eslint-plugin-import": "^2.
|
|
61
|
+
"@shikijs/vitepress-twoslash": "^3.9.1",
|
|
62
|
+
"@types/node": "^22.17.0",
|
|
63
|
+
"@vitest/eslint-plugin": "^1.3.4",
|
|
64
|
+
"bumpp": "^10.2.2",
|
|
65
|
+
"eslint": "^9.32.0",
|
|
66
|
+
"eslint-config-prettier": "^10.1.8",
|
|
67
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
68
|
+
"eslint-plugin-import": "^2.32.0",
|
|
69
|
+
"eslint-plugin-jsdoc": "^52.0.2",
|
|
69
70
|
"eslint-plugin-jsonc": "^2.20.1",
|
|
71
|
+
"eslint-plugin-markdown-preferences": "^0.4.0",
|
|
70
72
|
"eslint-plugin-module-interop": "^0.3.1",
|
|
71
73
|
"eslint-plugin-promise": "^7.2.1",
|
|
72
|
-
"eslint-plugin-regexp": "^2.
|
|
73
|
-
"eslint-plugin-unicorn": "^
|
|
74
|
+
"eslint-plugin-regexp": "^2.9.1",
|
|
75
|
+
"eslint-plugin-unicorn": "^60.0.0",
|
|
74
76
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
75
77
|
"eslint-plugin-yml": "^1.18.0",
|
|
76
|
-
"eslint-vitest-rule-tester": "^2.2.
|
|
78
|
+
"eslint-vitest-rule-tester": "^2.2.1",
|
|
77
79
|
"gh-changelogen": "^0.2.8",
|
|
78
|
-
"knip": "^5.
|
|
79
|
-
"lint-staged": "^16.
|
|
80
|
-
"pkg-pr-new": "^0.0.
|
|
81
|
-
"prettier": "^3.
|
|
80
|
+
"knip": "^5.62.0",
|
|
81
|
+
"lint-staged": "^16.1.2",
|
|
82
|
+
"pkg-pr-new": "^0.0.54",
|
|
83
|
+
"prettier": "^3.6.2",
|
|
82
84
|
"publint": "^0.3.12",
|
|
83
|
-
"tsdown": "^0.
|
|
84
|
-
"tsx": "^4.
|
|
85
|
-
"twoslash-eslint": "^0.3.
|
|
86
|
-
"typescript": "^5.
|
|
87
|
-
"typescript-eslint": "^8.
|
|
88
|
-
"vite-plugin-eslint4b": "^0.
|
|
85
|
+
"tsdown": "^0.13.2",
|
|
86
|
+
"tsx": "^4.20.3",
|
|
87
|
+
"twoslash-eslint": "^0.3.3",
|
|
88
|
+
"typescript": "^5.9.2",
|
|
89
|
+
"typescript-eslint": "^8.38.0",
|
|
90
|
+
"vite-plugin-eslint4b": "^0.6.0",
|
|
89
91
|
"vitepress": "^1.6.3",
|
|
90
|
-
"vitepress-plugin-group-icons": "^1.6.
|
|
91
|
-
"vitest": "^3.2.
|
|
92
|
+
"vitepress-plugin-group-icons": "^1.6.1",
|
|
93
|
+
"vitest": "^3.2.4"
|
|
92
94
|
},
|
|
93
95
|
"prettier": "@kazupon/prettier-config",
|
|
94
96
|
"lint-staged": {
|
|
@@ -118,11 +120,11 @@
|
|
|
118
120
|
"fix": "pnpm run --stream --color \"/^fix:/\"",
|
|
119
121
|
"fix:eslint": "eslint . --fix",
|
|
120
122
|
"fix:knip": "knip --fix --no-exit-code",
|
|
121
|
-
"fix:prettier": "prettier . --write",
|
|
123
|
+
"fix:prettier": "prettier . --write --experimental-cli",
|
|
122
124
|
"lint": "pnpm run --stream --color \"/^lint:/\"",
|
|
123
125
|
"lint:eslint": "eslint .",
|
|
124
126
|
"lint:knip": "knip",
|
|
125
|
-
"lint:prettier": "prettier . --check",
|
|
127
|
+
"lint:prettier": "prettier . --check --experimental-cli",
|
|
126
128
|
"release": "bumpp --commit \"release: v%s\" --all --push --tag",
|
|
127
129
|
"test": "vitest run",
|
|
128
130
|
"typecheck": "pnpm run --stream --color \"/^typecheck:/\"",
|