jsdoczoom 0.1.0 → 0.3.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.
@@ -7,6 +7,31 @@ import tsParser from "@typescript-eslint/parser";
7
7
  import { ESLint } from "eslint";
8
8
  import jsdocPlugin from "eslint-plugin-jsdoc";
9
9
  import plugin from "./eslint-plugin.js";
10
+
11
+ /** Common invalid JSDoc tags and their recommended replacements */
12
+ const TAG_MIGRATION_HINTS = {
13
+ "@remarks": "Move content to the description paragraph (prose before tags)",
14
+ "@packageDocumentation": "Use @module instead",
15
+ "@concept": "Move content to the description paragraph",
16
+ "@constraint": "Move content to the description paragraph",
17
+ "@vitest-environment":
18
+ "Use a plain comment instead: // @vitest-environment node",
19
+ };
20
+ /**
21
+ * Enhance a check-tag-names diagnostic message with a migration hint
22
+ * if the invalid tag is a commonly encountered one.
23
+ *
24
+ * @param message - Original ESLint diagnostic message
25
+ * @returns Enhanced message with hint, or original message if no hint available
26
+ */
27
+ function enhanceTagNameMessage(message) {
28
+ for (const [tag, hint] of Object.entries(TAG_MIGRATION_HINTS)) {
29
+ if (message.includes(tag)) {
30
+ return `${message} (Hint: ${hint})`;
31
+ }
32
+ }
33
+ return message;
34
+ }
10
35
  /**
11
36
  * Creates an ESLint instance configured for validation mode.
12
37
  *
@@ -15,26 +40,26 @@ import plugin from "./eslint-plugin.js";
15
40
  * @returns Configured ESLint instance for validation
16
41
  */
17
42
  export function createValidationLinter() {
18
- const eslint = new ESLint({
19
- overrideConfigFile: true,
20
- overrideConfig: [
21
- {
22
- files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
23
- plugins: { jsdoczoom: plugin },
24
- rules: {
25
- "jsdoczoom/require-file-jsdoc": "error",
26
- "jsdoczoom/require-file-summary": "error",
27
- "jsdoczoom/require-file-description": "error",
28
- },
29
- languageOptions: {
30
- parser: tsParser,
31
- ecmaVersion: "latest",
32
- sourceType: "module",
33
- },
34
- },
35
- ],
36
- });
37
- return eslint;
43
+ const eslint = new ESLint({
44
+ overrideConfigFile: true,
45
+ overrideConfig: [
46
+ {
47
+ files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
48
+ plugins: { jsdoczoom: plugin },
49
+ rules: {
50
+ "jsdoczoom/require-file-jsdoc": "error",
51
+ "jsdoczoom/require-file-summary": "error",
52
+ "jsdoczoom/require-file-description": "error",
53
+ },
54
+ languageOptions: {
55
+ parser: tsParser,
56
+ ecmaVersion: "latest",
57
+ sourceType: "module",
58
+ },
59
+ },
60
+ ],
61
+ });
62
+ return eslint;
38
63
  }
39
64
  /**
40
65
  * Creates an ESLint instance configured for lint mode.
@@ -45,40 +70,41 @@ export function createValidationLinter() {
45
70
  * @returns Configured ESLint instance for lint mode
46
71
  */
47
72
  export function createLintLinter(cwd) {
48
- const eslint = new ESLint({
49
- cwd,
50
- overrideConfigFile: true,
51
- overrideConfig: [
52
- {
53
- files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
54
- plugins: { jsdoczoom: plugin, jsdoc: jsdocPlugin },
55
- rules: {
56
- "jsdoczoom/require-file-jsdoc": "error",
57
- "jsdoczoom/require-file-summary": "error",
58
- "jsdoczoom/require-file-description": "error",
59
- "jsdoc/require-jsdoc": ["error", { publicOnly: true }],
60
- "jsdoc/require-param": "warn",
61
- "jsdoc/require-param-description": "warn",
62
- "jsdoc/require-returns": "warn",
63
- "jsdoc/require-returns-description": "warn",
64
- "jsdoc/require-throws": "warn",
65
- "jsdoc/check-param-names": "error",
66
- "jsdoc/check-tag-names": "error",
67
- "jsdoc/no-types": "error",
68
- "jsdoc/informative-docs": "error",
69
- "jsdoc/tag-lines": "off",
70
- "jsdoc/no-blank-blocks": "error",
71
- "jsdoc/require-description": "error",
72
- },
73
- languageOptions: {
74
- parser: tsParser,
75
- ecmaVersion: "latest",
76
- sourceType: "module",
77
- },
78
- },
79
- ],
80
- });
81
- return eslint;
73
+ const eslint = new ESLint({
74
+ cwd,
75
+ overrideConfigFile: true,
76
+ overrideConfig: [
77
+ {
78
+ files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
79
+ plugins: { jsdoczoom: plugin, jsdoc: jsdocPlugin },
80
+ rules: {
81
+ "jsdoczoom/require-file-jsdoc": "error",
82
+ "jsdoczoom/require-file-summary": "error",
83
+ "jsdoczoom/require-file-description": "error",
84
+ "jsdoczoom/require-file-ordering": "warn",
85
+ "jsdoc/require-jsdoc": ["error", { publicOnly: true }],
86
+ "jsdoc/require-param": "warn",
87
+ "jsdoc/require-param-description": "warn",
88
+ "jsdoc/require-returns": "warn",
89
+ "jsdoc/require-returns-description": "warn",
90
+ "jsdoc/require-throws": "warn",
91
+ "jsdoc/check-param-names": "error",
92
+ "jsdoc/check-tag-names": "error",
93
+ "jsdoc/no-types": "error",
94
+ "jsdoc/informative-docs": "error",
95
+ "jsdoc/tag-lines": "off",
96
+ "jsdoc/no-blank-blocks": "error",
97
+ "jsdoc/require-description": "error",
98
+ },
99
+ languageOptions: {
100
+ parser: tsParser,
101
+ ecmaVersion: "latest",
102
+ sourceType: "module",
103
+ },
104
+ },
105
+ ],
106
+ });
107
+ return eslint;
82
108
  }
83
109
  /**
84
110
  * Lints source text for validation mode and returns simplified messages.
@@ -89,14 +115,85 @@ export function createLintLinter(cwd) {
89
115
  * @returns Simplified message list with ruleId, messageId, and fatal flag
90
116
  */
91
117
  export async function lintFileForValidation(eslint, sourceText, filePath) {
92
- const results = await eslint.lintText(sourceText, { filePath });
93
- if (results.length === 0)
94
- return [];
95
- return results[0].messages.map((msg) => ({
96
- ruleId: msg.ruleId,
97
- messageId: msg.messageId,
98
- fatal: msg.fatal,
99
- }));
118
+ const results = await eslint.lintText(sourceText, { filePath });
119
+ if (results.length === 0) return [];
120
+ return results[0].messages.map((msg) => ({
121
+ ruleId: msg.ruleId,
122
+ messageId: msg.messageId,
123
+ fatal: msg.fatal,
124
+ }));
125
+ }
126
+ /**
127
+ * Extract the nearest symbol name from source text at a given line.
128
+ * Scans from the diagnostic line downward (up to 3 lines) for
129
+ * function, class, method, getter/setter, interface, type alias,
130
+ * or variable declarations. Skips lines that look like method calls
131
+ * (contain a dot before the identifier) to avoid false matches.
132
+ *
133
+ * @param sourceText - Full file source text
134
+ * @param line - 1-based line number from the diagnostic
135
+ * @returns Symbol name if found, undefined otherwise
136
+ */
137
+ function extractSymbolName(sourceText, line) {
138
+ const lines = sourceText.split("\n");
139
+ const searchEnd = Math.min(line + 2, lines.length);
140
+ for (let i = line - 1; i <= searchEnd; i++) {
141
+ const text = lines[i];
142
+ if (text === undefined) continue;
143
+ // Skip lines that are clearly method calls (e.g., `obj.method(`)
144
+ if (/\.\w+\s*\(/.test(text)) continue;
145
+ // Function declarations: function foo(, async function foo(, export function foo(
146
+ const funcMatch = text.match(
147
+ /(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
148
+ );
149
+ if (funcMatch?.[1]) return funcMatch[1];
150
+ // Class declarations: class Foo, export class Foo, abstract class Foo
151
+ const classMatch = text.match(
152
+ /(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/,
153
+ );
154
+ if (classMatch?.[1]) return classMatch[1];
155
+ // Interface declarations: interface Foo, export interface Foo
156
+ const ifaceMatch = text.match(/(?:export\s+)?interface\s+(\w+)/);
157
+ if (ifaceMatch?.[1]) return ifaceMatch[1];
158
+ // Type alias declarations: type Foo =, export type Foo =
159
+ const typeMatch = text.match(/(?:export\s+)?type\s+(\w+)\s*[=<]/);
160
+ if (typeMatch?.[1]) return typeMatch[1];
161
+ // Variable declarations: const foo =, let foo =, var foo =
162
+ const varMatch = text.match(
163
+ /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*[=:]/,
164
+ );
165
+ if (varMatch?.[1]) return varMatch[1];
166
+ // Getter/setter: get foo(), set foo()
167
+ const accessorMatch = text.match(/(?:get|set)\s+(\w+)\s*\(/);
168
+ if (accessorMatch?.[1]) return accessorMatch[1];
169
+ // Class method: identifier followed by ( but NOT preceded by a dot
170
+ // Only match if the line starts with optional whitespace + optional modifiers
171
+ const methodMatch = text.match(
172
+ /^\s*(?:(?:public|private|protected|static|readonly|async|override)\s+)*(\w+)\s*\(/,
173
+ );
174
+ if (methodMatch?.[1]) {
175
+ const name = methodMatch[1];
176
+ // Skip common control flow keywords
177
+ if (
178
+ ![
179
+ "if",
180
+ "for",
181
+ "while",
182
+ "switch",
183
+ "catch",
184
+ "return",
185
+ "import",
186
+ "from",
187
+ "new",
188
+ "await",
189
+ "throw",
190
+ ].includes(name)
191
+ ) {
192
+ return name;
193
+ }
194
+ }
195
+ }
196
+ return undefined;
100
197
  }
101
198
  /**
102
199
  * Lints source text for lint mode and returns detailed diagnostics.
@@ -107,16 +204,25 @@ export async function lintFileForValidation(eslint, sourceText, filePath) {
107
204
  * @returns Array of lint diagnostics with line, column, rule, message, and severity
108
205
  */
109
206
  export async function lintFileForLint(eslint, sourceText, filePath) {
110
- const results = await eslint.lintText(sourceText, { filePath });
111
- if (results.length === 0)
112
- return [];
113
- return results[0].messages.map((msg) => ({
114
- line: msg.line,
115
- column: msg.column,
116
- rule: msg.ruleId ?? "unknown",
117
- message: msg.message,
118
- severity: msg.severity === 2 ? "error" : "warning",
119
- }));
207
+ const results = await eslint.lintText(sourceText, { filePath });
208
+ if (results.length === 0) return [];
209
+ return results[0].messages.map((msg) => {
210
+ const diagnostic = {
211
+ line: msg.line,
212
+ column: msg.column,
213
+ rule: msg.ruleId ?? "unknown",
214
+ message:
215
+ msg.ruleId === "jsdoc/check-tag-names"
216
+ ? enhanceTagNameMessage(msg.message)
217
+ : msg.message,
218
+ severity: msg.severity === 2 ? "error" : "warning",
219
+ };
220
+ const symbol = extractSymbolName(sourceText, msg.line);
221
+ if (symbol) {
222
+ diagnostic.symbol = symbol;
223
+ }
224
+ return diagnostic;
225
+ });
120
226
  }
121
227
  /**
122
228
  * Maps ESLint messages to a single ValidationStatus using priority order.
@@ -133,28 +239,40 @@ export async function lintFileForLint(eslint, sourceText, filePath) {
133
239
  * @returns ValidationStatus or "valid"
134
240
  */
135
241
  export function mapToValidationStatus(messages) {
136
- // Priority 1: Parse errors
137
- if (messages.some((msg) => msg.ruleId === null && msg.fatal)) {
138
- return "syntax_error";
139
- }
140
- // Priority 2: Missing JSDoc
141
- if (messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-jsdoc")) {
142
- return "missing_jsdoc";
143
- }
144
- // Priority 3: Missing summary
145
- if (messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-summary" &&
146
- msg.messageId === "missingSummary")) {
147
- return "missing_summary";
148
- }
149
- // Priority 4: Multiple summary
150
- if (messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-summary" &&
151
- msg.messageId === "multipleSummary")) {
152
- return "multiple_summary";
153
- }
154
- // Priority 5: Missing description
155
- if (messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-description")) {
156
- return "missing_description";
157
- }
158
- // No matches
159
- return "valid";
242
+ // Priority 1: Parse errors
243
+ if (messages.some((msg) => msg.ruleId === null && msg.fatal)) {
244
+ return "syntax_error";
245
+ }
246
+ // Priority 2: Missing JSDoc
247
+ if (messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-jsdoc")) {
248
+ return "missing_jsdoc";
249
+ }
250
+ // Priority 3: Missing summary
251
+ if (
252
+ messages.some(
253
+ (msg) =>
254
+ msg.ruleId === "jsdoczoom/require-file-summary" &&
255
+ msg.messageId === "missingSummary",
256
+ )
257
+ ) {
258
+ return "missing_summary";
259
+ }
260
+ // Priority 4: Multiple summary
261
+ if (
262
+ messages.some(
263
+ (msg) =>
264
+ msg.ruleId === "jsdoczoom/require-file-summary" &&
265
+ msg.messageId === "multipleSummary",
266
+ )
267
+ ) {
268
+ return "multiple_summary";
269
+ }
270
+ // Priority 5: Missing description
271
+ if (
272
+ messages.some((msg) => msg.ruleId === "jsdoczoom/require-file-description")
273
+ ) {
274
+ return "missing_description";
275
+ }
276
+ // No matches
277
+ return "valid";
160
278
  }