@kazupon/eslint-plugin 0.2.2 → 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,9 +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 | | ⭐ |
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 | | ⭐ |
67
69
 
68
70
  <!--RULES_TABLE_END-->
69
71
 
package/lib/index.d.ts CHANGED
@@ -2,17 +2,19 @@ import { ESLint, Linter } from "eslint";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type PluginConfigs = {
5
- recommended: Linter.Config<Linter.RulesRecord>[]
6
- comment: Linter.Config<Linter.RulesRecord>[]
5
+ recommended: Linter.Config<Linter.RulesRecord>[];
6
+ comment: Linter.Config<Linter.RulesRecord>[];
7
7
  };
8
8
  declare const plugin: Omit<ESLint.Plugin, "configs"> & {
9
- configs: PluginConfigs
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
- comment: typeof commentConfig
14
+ recommended: typeof recommendedConfig;
15
+ comment: typeof commentConfig;
15
16
  };
17
+ /** @alias */
16
18
 
17
19
  //#endregion
18
20
  export { configs, plugin as default, plugin };
package/lib/index.js CHANGED
@@ -1,15 +1,28 @@
1
1
  import { parseComment } from "@es-joy/jsdoccomment";
2
2
 
3
3
  //#region src/utils/constants.ts
4
+ /**
5
+ * @author kazuya kawaguchi (a.k.a. kazupon)
6
+ * @license MIT
7
+ */
8
+ /**
9
+ * The plugin name.
10
+ */
4
11
  const name = "@kazupon/eslint-plugin";
5
- const version = "0.2.2";
12
+ /**
13
+ * The plugin version.
14
+ */
15
+ const version = "0.4.0";
16
+ /**
17
+ * The namespace for rules
18
+ */
6
19
  const namespace = "@kazupon";
7
20
 
8
21
  //#endregion
9
22
  //#region src/utils/rule.ts
10
23
  const BLOB_URL = "https://eslint-plugin.kazupon.dev/rules";
11
24
  function RuleCreator(urlCreator, namespace$1 = "") {
12
- return function createNamedRule({ meta, name: name$1,...rule$1 }) {
25
+ return function createNamedRule({ meta, name: name$1,...rule$3 }) {
13
26
  const ruleId = namespace$1 ? `${namespace$1}/${name$1}` : name$1;
14
27
  return {
15
28
  meta: {
@@ -21,7 +34,7 @@ function RuleCreator(urlCreator, namespace$1 = "") {
21
34
  ruleId
22
35
  }
23
36
  },
24
- ...rule$1
37
+ ...rule$3
25
38
  };
26
39
  };
27
40
  }
@@ -40,7 +53,7 @@ function initializeTagDiagnosis(tags) {
40
53
  function validTagDiagnosis(tagDiagnosis) {
41
54
  return Object.keys(tagDiagnosis).every((tag) => tagDiagnosis[tag] === "ok");
42
55
  }
43
- const rule = createRule({
56
+ const rule$2 = createRule({
44
57
  name: "enforce-header-comment",
45
58
  meta: {
46
59
  type: "suggestion",
@@ -168,11 +181,279 @@ const rule = createRule({
168
181
  };
169
182
  }
170
183
  });
171
- var enforce_header_comment_default = rule;
184
+ var enforce_header_comment_default = rule$2;
185
+
186
+ //#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
+ }
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({
256
+ name: "no-tag-comments",
257
+ meta: {
258
+ type: "problem",
259
+ docs: {
260
+ description: "disallow tag comments",
261
+ category: "Comment",
262
+ recommended: true,
263
+ defaultSeverity: "warn"
264
+ },
265
+ messages: { tagComment: "Exist '{{tag}}' tag comment" },
266
+ schema: [{
267
+ type: "object",
268
+ properties: { tags: {
269
+ type: "array",
270
+ items: { type: "string" },
271
+ minItems: 1,
272
+ uniqueItems: true
273
+ } },
274
+ additionalProperties: false
275
+ }]
276
+ },
277
+ create(ctx) {
278
+ const options = ctx.options[0] || { tags: DEFAULT_TAGS$1 };
279
+ const tags = options.tags || DEFAULT_TAGS$1;
280
+ const sourceCode = ctx.sourceCode;
281
+ /**
282
+ * Report a tag comment violation
283
+ */
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",
301
+ data: { tag },
302
+ loc: comment.loc
303
+ });
304
+ }
305
+ /**
306
+ * Check a comment for tag violations
307
+ */
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
+ }
334
+ }
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;
379
+ /**
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
413
+ */
414
+ function checkComment(comment) {
415
+ const { value, type } = comment;
416
+ if (type === "Line") {
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
+ }
425
+ return;
426
+ }
427
+ const lines = value.split("\n");
428
+ for (const [i, line] of lines.entries()) {
429
+ const trimmedLine = line.trim();
430
+ if (!trimmedLine) continue;
431
+ const contentToCheck = stripJSDocPrefix(line);
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
+ }
440
+ }
441
+ }
442
+ return { Program() {
443
+ const comments = sourceCode.getAllComments();
444
+ for (const comment of comments) checkComment(comment);
445
+ } };
446
+ }
447
+ });
448
+ var prefer_scope_on_tag_comment_default = rule;
172
449
 
173
450
  //#endregion
174
451
  //#region src/rules/index.ts
175
- const rules = { "enforce-header-comment": enforce_header_comment_default };
452
+ const rules = {
453
+ "enforce-header-comment": enforce_header_comment_default,
454
+ "no-tag-comments": no_tag_comments_default,
455
+ "prefer-scope-on-tag-comment": prefer_scope_on_tag_comment_default
456
+ };
176
457
 
177
458
  //#endregion
178
459
  //#region src/index.ts
@@ -184,6 +465,23 @@ const plugin = {
184
465
  rules,
185
466
  configs: {}
186
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
+ }];
187
485
  const commentConfig = [{
188
486
  name: "@kazupon/eslint-plugin/comment",
189
487
  files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
@@ -193,17 +491,18 @@ const commentConfig = [{
193
491
  "**/*.config.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
194
492
  ],
195
493
  plugins: { [namespace]: plugin },
196
- rules: Object.entries(rules).reduce((rules$1, [ruleName, rule$1]) => {
197
- const ruleId = rule$1.meta?.docs?.ruleId || (namespace ? `${namespace}/${ruleName}` : ruleName);
198
- rules$1[ruleId] = rule$1.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";
199
497
  return rules$1;
200
498
  }, Object.create(null))
201
499
  }];
202
500
  const configs = {
203
- recommended: [...commentConfig],
501
+ recommended: recommendedConfig,
204
502
  comment: commentConfig
205
503
  };
206
504
  plugin.configs = configs;
505
+ /** @alias */
207
506
  var src_default = plugin;
208
507
 
209
508
  //#endregion
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.2.2",
4
+ "version": "0.4.0",
5
5
  "license": "MIT",
6
6
  "funding": "https://github.com/sponsors/kazupon",
7
7
  "bugs": {
@@ -47,46 +47,48 @@
47
47
  }
48
48
  },
49
49
  "dependencies": {
50
- "@es-joy/jsdoccomment": "^0.50.0",
51
- "@eslint/core": "^0.13.0"
50
+ "@es-joy/jsdoccomment": "^0.50.2",
51
+ "@eslint/core": "^0.14.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "eslint": "^9.0.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@eslint/markdown": "^6.3.0",
58
- "@kazupon/eslint-config": "^0.28.0",
57
+ "@eslint/compat": "^1.2.9",
58
+ "@eslint/markdown": "^6.5.0",
59
+ "@kazupon/eslint-config": "^0.31.0",
59
60
  "@kazupon/prettier-config": "^0.1.1",
60
- "@shikijs/vitepress-twoslash": "^3.2.2",
61
- "@types/node": "^22.14.1",
62
- "@vitest/eslint-plugin": "^1.1.42",
63
- "bumpp": "^10.1.0",
64
- "eslint": "^9.24.0",
65
- "eslint-config-prettier": "^10.1.2",
66
- "eslint-import-resolver-typescript": "^4.3.2",
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",
67
68
  "eslint-plugin-import": "^2.31.0",
68
- "eslint-plugin-jsonc": "^2.20.0",
69
+ "eslint-plugin-jsonc": "^2.20.1",
69
70
  "eslint-plugin-module-interop": "^0.3.1",
70
71
  "eslint-plugin-promise": "^7.2.1",
71
- "eslint-plugin-unicorn": "^58.0.0",
72
+ "eslint-plugin-regexp": "^2.8.0",
73
+ "eslint-plugin-unicorn": "^59.0.0",
72
74
  "eslint-plugin-unused-imports": "^4.1.4",
73
- "eslint-plugin-yml": "^1.17.0",
75
+ "eslint-plugin-yml": "^1.18.0",
74
76
  "eslint-vitest-rule-tester": "^2.2.0",
75
77
  "gh-changelogen": "^0.2.8",
76
- "knip": "^5.50.2",
77
- "lint-staged": "^15.5.1",
78
- "pkg-pr-new": "^0.0.42",
78
+ "knip": "^5.60.2",
79
+ "lint-staged": "^16.0.0",
80
+ "pkg-pr-new": "^0.0.51",
79
81
  "prettier": "^3.5.3",
80
- "publint": "^0.3.11",
81
- "tsdown": "^0.8.0-beta.2",
82
- "tsx": "^4.19.3",
82
+ "publint": "^0.3.12",
83
+ "tsdown": "^0.12.7",
84
+ "tsx": "^4.19.4",
83
85
  "twoslash-eslint": "^0.3.1",
84
86
  "typescript": "^5.8.3",
85
- "typescript-eslint": "^8.29.1",
87
+ "typescript-eslint": "^8.33.1",
86
88
  "vite-plugin-eslint4b": "^0.5.1",
87
89
  "vitepress": "^1.6.3",
88
- "vitepress-plugin-group-icons": "^1.4.1",
89
- "vitest": "^3.1.1"
90
+ "vitepress-plugin-group-icons": "^1.6.0",
91
+ "vitest": "^3.2.2"
90
92
  },
91
93
  "prettier": "@kazupon/prettier-config",
92
94
  "lint-staged": {