@kazupon/eslint-plugin 0.3.0 → 0.4.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,10 +61,11 @@ 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 | | |
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 | | ⭐ |
68
69
 
69
70
  <!--RULES_TABLE_END-->
70
71
 
package/lib/index.d.ts CHANGED
@@ -8,9 +8,10 @@ type PluginConfigs = {
8
8
  declare const plugin: Omit<ESLint.Plugin, "configs"> & {
9
9
  configs: PluginConfigs;
10
10
  };
11
+ declare const recommendedConfig: Linter.Config[];
11
12
  declare const commentConfig: Linter.Config[];
12
13
  declare const configs: {
13
- recommended: typeof commentConfig;
14
+ recommended: typeof recommendedConfig;
14
15
  comment: typeof commentConfig;
15
16
  };
16
17
  /** @alias */
package/lib/index.js CHANGED
@@ -12,7 +12,7 @@ const name = "@kazupon/eslint-plugin";
12
12
  /**
13
13
  * The plugin version.
14
14
  */
15
- const version = "0.3.0";
15
+ const version = "0.4.0";
16
16
  /**
17
17
  * The namespace for rules
18
18
  */
@@ -22,7 +22,7 @@ const namespace = "@kazupon";
22
22
  //#region src/utils/rule.ts
23
23
  const BLOB_URL = "https://eslint-plugin.kazupon.dev/rules";
24
24
  function RuleCreator(urlCreator, namespace$1 = "") {
25
- return function createNamedRule({ meta, name: name$1,...rule$2 }) {
25
+ return function createNamedRule({ meta, name: name$1,...rule$3 }) {
26
26
  const ruleId = namespace$1 ? `${namespace$1}/${name$1}` : name$1;
27
27
  return {
28
28
  meta: {
@@ -34,7 +34,7 @@ function RuleCreator(urlCreator, namespace$1 = "") {
34
34
  ruleId
35
35
  }
36
36
  },
37
- ...rule$2
37
+ ...rule$3
38
38
  };
39
39
  };
40
40
  }
@@ -53,7 +53,7 @@ function initializeTagDiagnosis(tags) {
53
53
  function validTagDiagnosis(tagDiagnosis) {
54
54
  return Object.keys(tagDiagnosis).every((tag) => tagDiagnosis[tag] === "ok");
55
55
  }
56
- const rule$1 = createRule({
56
+ const rule$2 = createRule({
57
57
  name: "enforce-header-comment",
58
58
  meta: {
59
59
  type: "suggestion",
@@ -181,11 +181,10 @@ const rule$1 = createRule({
181
181
  };
182
182
  }
183
183
  });
184
- var enforce_header_comment_default = rule$1;
184
+ var enforce_header_comment_default = rule$2;
185
185
 
186
186
  //#endregion
187
- //#region src/rules/no-tag-comments.ts
188
- const DEFAULT_TAGS = ["FIXME", "BUG"];
187
+ //#region src/utils/comment.ts
189
188
  /**
190
189
  * Remove JSDoc asterisk prefix if present
191
190
  */
@@ -193,17 +192,77 @@ function stripJSDocPrefix(line) {
193
192
  const trimmed = line.trim();
194
193
  return trimmed.startsWith("*") ? trimmed.slice(1).trim() : trimmed;
195
194
  }
196
- const rule = createRule({
195
+ /**
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
200
+ */
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
+ /**
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
236
+ */
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
+ };
250
+ }
251
+
252
+ //#endregion
253
+ //#region src/rules/no-tag-comments.ts
254
+ const DEFAULT_TAGS$1 = ["FIXME", "BUG"];
255
+ const rule$1 = createRule({
197
256
  name: "no-tag-comments",
198
257
  meta: {
199
258
  type: "problem",
200
259
  docs: {
201
260
  description: "disallow tag comments",
202
261
  category: "Comment",
203
- recommended: false,
262
+ recommended: true,
204
263
  defaultSeverity: "warn"
205
264
  },
206
- messages: { avoidTagComment: "Fix '{{tag}}' tag comment" },
265
+ messages: { tagComment: "Exist '{{tag}}' tag comment" },
207
266
  schema: [{
208
267
  type: "object",
209
268
  properties: { tags: {
@@ -216,59 +275,168 @@ const rule = createRule({
216
275
  }]
217
276
  },
218
277
  create(ctx) {
219
- const options = ctx.options[0] || { tags: DEFAULT_TAGS };
220
- const tags = options.tags || DEFAULT_TAGS;
278
+ const options = ctx.options[0] || { tags: DEFAULT_TAGS$1 };
279
+ const tags = options.tags || DEFAULT_TAGS$1;
221
280
  const sourceCode = ctx.sourceCode;
222
281
  /**
223
- * Check if the text starts with a tag followed by valid delimiter
224
- */
225
- function hasTag(text) {
226
- for (const tag of tags) if (text.startsWith(tag)) {
227
- const afterTag = text.slice(tag.length);
228
- if (afterTag === "" || afterTag.startsWith(":") || afterTag.startsWith(" ")) return tag;
229
- }
230
- return null;
231
- }
232
- /**
233
282
  * Report a tag comment violation
234
283
  */
235
- function reportTag(comment, tag) {
236
- ctx.report({
237
- messageId: "avoidTagComment",
284
+ 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",
238
301
  data: { tag },
239
302
  loc: comment.loc
240
303
  });
241
304
  }
242
305
  /**
243
- * Process a single line of text for tags
306
+ * Check a comment for tag violations
244
307
  */
245
- function checkLine(text, comment) {
246
- const tag = hasTag(text);
247
- if (tag) {
248
- reportTag(comment, tag);
249
- return true;
308
+ function checkComment(comment) {
309
+ const { value, type } = comment;
310
+ if (type === "Line") {
311
+ const tagInfo = detectTag(value.trim(), tags);
312
+ if (tagInfo) {
313
+ const tagIndex = value.indexOf(tagInfo.tag);
314
+ if (tagIndex !== -1) reportTag(comment, tagInfo.tag, {
315
+ line: comment.loc.start.line,
316
+ column: comment.loc.start.column + 2 + tagIndex
317
+ });
318
+ }
319
+ return;
320
+ }
321
+ const lines = value.split("\n");
322
+ for (const [i, line] of lines.entries()) {
323
+ const trimmedLine = line.trim();
324
+ if (!trimmedLine) continue;
325
+ const contentToCheck = stripJSDocPrefix(line);
326
+ const tagInfo = detectTag(contentToCheck, tags);
327
+ if (tagInfo) {
328
+ const location = calculateTagLocation(comment, line, i, tagInfo.tag);
329
+ if (location) {
330
+ reportTag(comment, tagInfo.tag, location);
331
+ break;
332
+ }
333
+ }
250
334
  }
251
- return false;
252
335
  }
336
+ return { Program() {
337
+ const comments = sourceCode.getAllComments();
338
+ for (const comment of comments) checkComment(comment);
339
+ } };
340
+ }
341
+ });
342
+ var no_tag_comments_default = rule$1;
343
+
344
+ //#endregion
345
+ //#region src/rules/prefer-scope-on-tag-comment.ts
346
+ const DEFAULT_TAGS = [
347
+ "TODO",
348
+ "FIXME",
349
+ "HACK",
350
+ "BUG",
351
+ "NOTE"
352
+ ];
353
+ const rule = createRule({
354
+ name: "prefer-scope-on-tag-comment",
355
+ meta: {
356
+ type: "suggestion",
357
+ docs: {
358
+ description: "enforce adding a scope to tag comments",
359
+ category: "Comment",
360
+ recommended: true,
361
+ defaultSeverity: "warn"
362
+ },
363
+ messages: { missingScope: "Tag comment '{{tag}}' is missing a scope. Use format: {{tag}}(scope)" },
364
+ schema: [{
365
+ type: "object",
366
+ properties: { tags: {
367
+ type: "array",
368
+ items: { type: "string" },
369
+ minItems: 1,
370
+ uniqueItems: true
371
+ } },
372
+ additionalProperties: false
373
+ }]
374
+ },
375
+ create(ctx) {
376
+ const options = ctx.options[0] || { tags: DEFAULT_TAGS };
377
+ const tags = options.tags || DEFAULT_TAGS;
378
+ const sourceCode = ctx.sourceCode;
253
379
  /**
254
- * Check a comment for tag violations
380
+ * Report a missing scope violation
381
+ */
382
+ 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
+ });
410
+ }
411
+ /**
412
+ * Check a comment for missing scope
255
413
  */
256
414
  function checkComment(comment) {
257
415
  const { value, type } = comment;
258
416
  if (type === "Line") {
259
- checkLine(value.trim(), comment);
417
+ const tagInfo = detectTag(value.trim(), tags);
418
+ if (tagInfo && !tagInfo.hasScope) {
419
+ const tagIndex = value.indexOf(tagInfo.tag);
420
+ if (tagIndex !== -1) reportMissingScope(comment, tagInfo.tag, {
421
+ line: comment.loc.start.line,
422
+ column: comment.loc.start.column + 2 + tagIndex
423
+ });
424
+ }
260
425
  return;
261
426
  }
262
427
  const lines = value.split("\n");
263
- if (lines.length === 1) {
264
- checkLine(value.trim(), comment);
265
- return;
266
- }
267
- for (const line of lines) {
428
+ for (const [i, line] of lines.entries()) {
268
429
  const trimmedLine = line.trim();
269
430
  if (!trimmedLine) continue;
270
431
  const contentToCheck = stripJSDocPrefix(line);
271
- if (checkLine(contentToCheck, comment)) break;
432
+ const tagInfo = detectTag(contentToCheck, tags);
433
+ if (tagInfo && !tagInfo.hasScope) {
434
+ const location = calculateTagLocation(comment, line, i, tagInfo.tag);
435
+ if (location) {
436
+ reportMissingScope(comment, tagInfo.tag, location);
437
+ break;
438
+ }
439
+ }
272
440
  }
273
441
  }
274
442
  return { Program() {
@@ -277,13 +445,14 @@ const rule = createRule({
277
445
  } };
278
446
  }
279
447
  });
280
- var no_tag_comments_default = rule;
448
+ var prefer_scope_on_tag_comment_default = rule;
281
449
 
282
450
  //#endregion
283
451
  //#region src/rules/index.ts
284
452
  const rules = {
285
453
  "enforce-header-comment": enforce_header_comment_default,
286
- "no-tag-comments": no_tag_comments_default
454
+ "no-tag-comments": no_tag_comments_default,
455
+ "prefer-scope-on-tag-comment": prefer_scope_on_tag_comment_default
287
456
  };
288
457
 
289
458
  //#endregion
@@ -296,6 +465,23 @@ const plugin = {
296
465
  rules,
297
466
  configs: {}
298
467
  };
468
+ const recommendedConfig = [{
469
+ name: "@kazupon/eslint-plugin/recommended",
470
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
471
+ ignores: [
472
+ "**/*.md",
473
+ "**/*.md/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
474
+ "**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
475
+ ],
476
+ 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";
481
+ }
482
+ return acc;
483
+ }, Object.create(null))
484
+ }];
299
485
  const commentConfig = [{
300
486
  name: "@kazupon/eslint-plugin/comment",
301
487
  files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
@@ -305,14 +491,14 @@ const commentConfig = [{
305
491
  "**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
306
492
  ],
307
493
  plugins: { [namespace]: plugin },
308
- rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$2]) => {
309
- const ruleId = rule$2.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
310
- rules$1[ruleId] = rule$2.meta?.docs?.defaultSeverity || "warn";
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";
311
497
  return rules$1;
312
498
  }, Object.create(null))
313
499
  }];
314
500
  const configs = {
315
- recommended: [...commentConfig],
501
+ recommended: recommendedConfig,
316
502
  comment: commentConfig
317
503
  };
318
504
  plugin.configs = configs;
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.3.0",
4
+ "version": "0.4.0",
5
5
  "license": "MIT",
6
6
  "funding": "https://github.com/sponsors/kazupon",
7
7
  "bugs": {
@@ -56,7 +56,7 @@
56
56
  "devDependencies": {
57
57
  "@eslint/compat": "^1.2.9",
58
58
  "@eslint/markdown": "^6.5.0",
59
- "@kazupon/eslint-config": "^0.30.0",
59
+ "@kazupon/eslint-config": "^0.31.0",
60
60
  "@kazupon/prettier-config": "^0.1.1",
61
61
  "@shikijs/vitepress-twoslash": "^3.6.0",
62
62
  "@types/node": "^22.15.30",
@@ -69,7 +69,8 @@
69
69
  "eslint-plugin-jsonc": "^2.20.1",
70
70
  "eslint-plugin-module-interop": "^0.3.1",
71
71
  "eslint-plugin-promise": "^7.2.1",
72
- "eslint-plugin-unicorn": "^58.0.0",
72
+ "eslint-plugin-regexp": "^2.8.0",
73
+ "eslint-plugin-unicorn": "^59.0.0",
73
74
  "eslint-plugin-unused-imports": "^4.1.4",
74
75
  "eslint-plugin-yml": "^1.18.0",
75
76
  "eslint-vitest-rule-tester": "^2.2.0",