@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 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 | 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-scope-on-tag-comment](https://eslint-plugin.kazupon.dev/rules/prefer-scope-on-tag-comment.html) | enforce adding a scope to tag comments | Comment | | ⭐ |
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.4.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$3 }) {
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$3
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$2 = createRule({
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.report({
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$2;
370
+ var enforce_header_comment_default = rule$3;
185
371
 
186
372
  //#endregion
187
- //#region src/utils/comment.ts
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
- * Check if the text starts with any of the given tags
197
- * @param text The text to check
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
- * Calculate the exact location of a tag in a comment
231
- * @param comment The comment containing the tag
232
- * @param line The line of text containing the tag
233
- * @param lineIndex The index of the line within the comment
234
- * @param tag The tag to locate
235
- * @returns Location with line and column, or null if not found
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 calculateTagLocation(comment, line, lineIndex, tag) {
238
- const tagIndex = line.indexOf(tag);
239
- if (tagIndex === -1) return null;
240
- if (lineIndex === 0) {
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$1 = createRule({
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] || { tags: DEFAULT_TAGS$1 };
279
- const tags = options.tags || DEFAULT_TAGS$1;
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
- if (loc && comment.loc) ctx.report({
286
- messageId: "tagComment",
287
- data: { tag },
288
- loc: {
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
- const comments = sourceCode.getAllComments();
338
- for (const comment of comments) checkComment(comment);
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 no_tag_comments_default = rule$1;
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: { tags: {
367
- type: "array",
368
- items: { type: "string" },
369
- minItems: 1,
370
- uniqueItems: true
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] || { tags: DEFAULT_TAGS };
377
- const tags = options.tags || DEFAULT_TAGS;
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
- if (!comment.loc) {
384
- ctx.report({
385
- messageId: "missingScope",
386
- data: { tag },
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
- const comments = sourceCode.getAllComments();
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$3]) => {
478
- if (rule$3.meta?.docs?.recommended) {
479
- const ruleId = rule$3.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
480
- acc[ruleId] = rule$3.meta?.docs?.defaultSeverity || "warn";
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$3]) => {
495
- const ruleId = rule$3.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
496
- rules$1[ruleId] = rule$3.meta?.docs?.defaultSeverity || "warn";
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.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.50.2",
51
- "@eslint/core": "^0.14.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.2.9",
58
- "@eslint/markdown": "^6.5.0",
59
- "@kazupon/eslint-config": "^0.31.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.6.0",
62
- "@types/node": "^22.15.30",
63
- "@vitest/eslint-plugin": "^1.2.1",
64
- "bumpp": "^10.1.1",
65
- "eslint": "^9.28.0",
66
- "eslint-config-prettier": "^10.1.5",
67
- "eslint-import-resolver-typescript": "^4.4.3",
68
- "eslint-plugin-import": "^2.31.0",
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.8.0",
73
- "eslint-plugin-unicorn": "^59.0.0",
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.0",
78
+ "eslint-vitest-rule-tester": "^2.2.1",
77
79
  "gh-changelogen": "^0.2.8",
78
- "knip": "^5.60.2",
79
- "lint-staged": "^16.0.0",
80
- "pkg-pr-new": "^0.0.51",
81
- "prettier": "^3.5.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.12.7",
84
- "tsx": "^4.19.4",
85
- "twoslash-eslint": "^0.3.1",
86
- "typescript": "^5.8.3",
87
- "typescript-eslint": "^8.33.1",
88
- "vite-plugin-eslint4b": "^0.5.1",
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.0",
91
- "vitest": "^3.2.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:/\"",