@hatem427/code-guard-ci 3.0.0 → 3.2.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.
Files changed (102) hide show
  1. package/config/fastify.config.ts +326 -0
  2. package/config/hono.config.ts +331 -0
  3. package/config/nestjs.config.ts +500 -0
  4. package/config/node.config.ts +425 -0
  5. package/config/python.config.ts +512 -0
  6. package/dist/config/fastify.config.d.ts +17 -0
  7. package/dist/config/fastify.config.d.ts.map +1 -0
  8. package/dist/config/fastify.config.js +279 -0
  9. package/dist/config/fastify.config.js.map +1 -0
  10. package/dist/config/hono.config.d.ts +17 -0
  11. package/dist/config/hono.config.d.ts.map +1 -0
  12. package/dist/config/hono.config.js +287 -0
  13. package/dist/config/hono.config.js.map +1 -0
  14. package/dist/config/nestjs.config.d.ts +17 -0
  15. package/dist/config/nestjs.config.d.ts.map +1 -0
  16. package/dist/config/nestjs.config.js +440 -0
  17. package/dist/config/nestjs.config.js.map +1 -0
  18. package/dist/config/node.config.d.ts +17 -0
  19. package/dist/config/node.config.d.ts.map +1 -0
  20. package/dist/config/node.config.js +363 -0
  21. package/dist/config/node.config.js.map +1 -0
  22. package/dist/config/python.config.d.ts +15 -0
  23. package/dist/config/python.config.d.ts.map +1 -0
  24. package/dist/config/python.config.js +475 -0
  25. package/dist/config/python.config.js.map +1 -0
  26. package/dist/scripts/auto-fix.d.ts +5 -0
  27. package/dist/scripts/auto-fix.d.ts.map +1 -1
  28. package/dist/scripts/auto-fix.js +5 -0
  29. package/dist/scripts/auto-fix.js.map +1 -1
  30. package/dist/scripts/cli.js +2 -2
  31. package/dist/scripts/cli.js.map +1 -1
  32. package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
  33. package/dist/scripts/config-generators/ai-config-generator.js +6 -0
  34. package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
  35. package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
  36. package/dist/scripts/config-generators/eslint-generator.js +108 -0
  37. package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
  38. package/dist/scripts/config-generators/frameworks/fastify.d.ts +6 -0
  39. package/dist/scripts/config-generators/frameworks/fastify.d.ts.map +1 -0
  40. package/dist/scripts/config-generators/frameworks/fastify.js +68 -0
  41. package/dist/scripts/config-generators/frameworks/fastify.js.map +1 -0
  42. package/dist/scripts/config-generators/frameworks/hono.d.ts +6 -0
  43. package/dist/scripts/config-generators/frameworks/hono.d.ts.map +1 -0
  44. package/dist/scripts/config-generators/frameworks/hono.js +63 -0
  45. package/dist/scripts/config-generators/frameworks/hono.js.map +1 -0
  46. package/dist/scripts/config-generators/frameworks/index.d.ts +3 -0
  47. package/dist/scripts/config-generators/frameworks/index.d.ts.map +1 -1
  48. package/dist/scripts/config-generators/frameworks/index.js +7 -1
  49. package/dist/scripts/config-generators/frameworks/index.js.map +1 -1
  50. package/dist/scripts/config-generators/frameworks/nestjs.d.ts +6 -0
  51. package/dist/scripts/config-generators/frameworks/nestjs.d.ts.map +1 -0
  52. package/dist/scripts/config-generators/frameworks/nestjs.js +83 -0
  53. package/dist/scripts/config-generators/frameworks/nestjs.js.map +1 -0
  54. package/dist/scripts/config-generators/frameworks/node.d.ts +2 -2
  55. package/dist/scripts/config-generators/frameworks/node.d.ts.map +1 -1
  56. package/dist/scripts/config-generators/frameworks/node.js +56 -11
  57. package/dist/scripts/config-generators/frameworks/node.js.map +1 -1
  58. package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
  59. package/dist/scripts/config-generators/typescript-generator.js +33 -0
  60. package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
  61. package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
  62. package/dist/scripts/config-generators/vscode-generator.js +73 -0
  63. package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
  64. package/dist/scripts/generate-pr-checklist.d.ts +5 -0
  65. package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
  66. package/dist/scripts/generate-pr-checklist.js +6 -1
  67. package/dist/scripts/generate-pr-checklist.js.map +1 -1
  68. package/dist/scripts/postinstall.js +38 -0
  69. package/dist/scripts/postinstall.js.map +1 -1
  70. package/dist/scripts/precommit-check.d.ts +13 -0
  71. package/dist/scripts/precommit-check.d.ts.map +1 -1
  72. package/dist/scripts/precommit-check.js +288 -5
  73. package/dist/scripts/precommit-check.js.map +1 -1
  74. package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
  75. package/dist/scripts/utils/naming-validator.js +96 -2
  76. package/dist/scripts/utils/naming-validator.js.map +1 -1
  77. package/dist/scripts/utils/project-detector.d.ts +12 -9
  78. package/dist/scripts/utils/project-detector.d.ts.map +1 -1
  79. package/dist/scripts/utils/project-detector.js +63 -11
  80. package/dist/scripts/utils/project-detector.js.map +1 -1
  81. package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
  82. package/dist/scripts/utils/structure-validator.js +50 -0
  83. package/dist/scripts/utils/structure-validator.js.map +1 -1
  84. package/package.json +11 -3
  85. package/scripts/auto-fix.ts +5 -0
  86. package/scripts/cli.ts +2 -2
  87. package/scripts/config-generators/ai-config-generator.ts +9 -0
  88. package/scripts/config-generators/eslint-generator.ts +110 -0
  89. package/scripts/config-generators/frameworks/fastify.ts +65 -0
  90. package/scripts/config-generators/frameworks/hono.ts +60 -0
  91. package/scripts/config-generators/frameworks/index.ts +3 -0
  92. package/scripts/config-generators/frameworks/nestjs.ts +80 -0
  93. package/scripts/config-generators/frameworks/node.ts +57 -11
  94. package/scripts/config-generators/typescript-generator.ts +36 -0
  95. package/scripts/config-generators/vscode-generator.ts +84 -0
  96. package/scripts/generate-pr-checklist.ts +6 -1
  97. package/scripts/postinstall.ts +38 -0
  98. package/scripts/precommit-check.ts +334 -6
  99. package/scripts/utils/naming-validator.ts +104 -2
  100. package/scripts/utils/project-detector.ts +78 -11
  101. package/scripts/utils/structure-validator.ts +54 -0
  102. package/templates/feature-doc-backend.md +114 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * ============================================================================
3
+ * fastify.config.ts — Fastify-specific coding rules
4
+ * ============================================================================
5
+ *
6
+ * Registers rules that apply to Fastify projects:
7
+ * - Schema-first validation enforcement
8
+ * - Plugin scope correctness
9
+ * - Async handler patterns
10
+ * - Security: CORS, helmet, rate limiting
11
+ * - Error response shape
12
+ * - Hook lifecycle usage
13
+ */
14
+
15
+ import { registerRules, Rule } from './guidelines.config';
16
+
17
+ const fastifyRules: Rule[] = [
18
+
19
+ // ── Schema-First Validation ───────────────────────────────────────────────
20
+
21
+ // ─────────────────────────────────────────
22
+ // RULE: fastify-schema-required
23
+ // ROLE: Enforce schema definitions on all routes
24
+ // PURPOSE: Fastify uses JSON Schema / TypeBox for both request validation
25
+ // and response serialization. Without schemas, inputs are not
26
+ // validated and responses include all properties (potential data leak).
27
+ // Schemas also enable Fastify's fast-json-stringify for ~2× serialization speed.
28
+ // EXAMPLE:
29
+ // WRONG:
30
+ // fastify.post('/users', async (req, reply) => {
31
+ // return userService.create(req.body);
32
+ // });
33
+ // RIGHT:
34
+ // fastify.post('/users', {
35
+ // schema: {
36
+ // body: CreateUserSchema,
37
+ // response: { 201: UserResponseSchema },
38
+ // },
39
+ // }, async (req, reply) => {
40
+ // return userService.create(req.body);
41
+ // });
42
+ // ─────────────────────────────────────────
43
+ {
44
+ id: 'fastify-schema-required',
45
+ label: 'Define schema for every route (body, params, response)',
46
+ description:
47
+ 'Every Fastify route must define a schema object with at minimum body/params validation and response schemas. This enables validation, serialization speed, and prevents data leaks.',
48
+ severity: 'error',
49
+ fileExtensions: ['ts', 'js'],
50
+ pattern: null,
51
+ customCheck: (file) => {
52
+ const violations: Array<{ line: number | null; message: string }> = [];
53
+
54
+ if (
55
+ !file.relativePath.includes('/routes/') &&
56
+ !file.relativePath.endsWith('.route.ts') &&
57
+ !file.relativePath.endsWith('.routes.ts')
58
+ ) {
59
+ return [];
60
+ }
61
+
62
+ // Look for fastify.post/put/patch/delete without a schema key
63
+ const routeRegex = /fastify\.\s*(?:post|put|patch|delete)\s*\(\s*['"`][^'"]+['"`]\s*,\s*async/g;
64
+ let match;
65
+ while ((match = routeRegex.exec(file.content)) !== null) {
66
+ const near = file.content.slice(Math.max(0, match.index - 200), match.index + 50);
67
+ if (!near.includes('schema')) {
68
+ const lineNum = file.content.slice(0, match.index).split('\n').length;
69
+ violations.push({
70
+ line: lineNum,
71
+ message:
72
+ 'Mutating route (POST/PUT/PATCH/DELETE) is missing a schema definition. Add body validation schema and response schema.',
73
+ });
74
+ }
75
+ }
76
+
77
+ return violations;
78
+ },
79
+ applicableTo: ['fastify'],
80
+ category: 'Validation',
81
+ },
82
+
83
+ // ─────────────────────────────────────────
84
+ // RULE: fastify-response-schema
85
+ // ROLE: Enforce response schema definitions
86
+ // PURPOSE: Without a response schema, Fastify falls back to slow JSON.stringify
87
+ // and may expose internal fields. Response schemas activate fast-json-stringify
88
+ // and act as a serialisation allowlist.
89
+ // EXAMPLE:
90
+ // WRONG:
91
+ // fastify.get('/users/:id', async (req, reply) => { return user; });
92
+ // RIGHT:
93
+ // fastify.get('/users/:id', {
94
+ // schema: { response: { 200: UserResponseSchema } },
95
+ // }, async (req, reply) => { return user; });
96
+ // ─────────────────────────────────────────
97
+ {
98
+ id: 'fastify-response-schema',
99
+ label: 'Always define a response schema',
100
+ description:
101
+ 'Define response schemas for all routes to enable fast-json-stringify serialization and prevent accidental exposure of sensitive fields.',
102
+ severity: 'warning',
103
+ fileExtensions: ['ts', 'js'],
104
+ pattern: null,
105
+ customCheck: (file) => {
106
+ const violations: Array<{ line: number | null; message: string }> = [];
107
+
108
+ if (
109
+ !file.relativePath.includes('/routes/') &&
110
+ !file.relativePath.endsWith('.route.ts') &&
111
+ !file.relativePath.endsWith('.routes.ts')
112
+ ) {
113
+ return [];
114
+ }
115
+
116
+ const hasSchema = /schema\s*:\s*\{/.test(file.content);
117
+ const hasResponseSchema = /response\s*:\s*\{/.test(file.content);
118
+
119
+ if (hasSchema && !hasResponseSchema) {
120
+ violations.push({
121
+ line: null,
122
+ message:
123
+ 'Route schema found but no response schema defined. Add response: { 200: Schema } to enable fast serialization and prevent data leaks.',
124
+ });
125
+ }
126
+
127
+ return violations;
128
+ },
129
+ applicableTo: ['fastify'],
130
+ category: 'Validation',
131
+ },
132
+
133
+ // ── Plugin Architecture ────────────────────────────────────────────────────
134
+
135
+ // ─────────────────────────────────────────
136
+ // RULE: fastify-use-fastify-plugin
137
+ // ROLE: Enforce plugin scope correctness
138
+ // PURPOSE: In Fastify, plugins registered without fastify-plugin are
139
+ // scoped to a child context — decorations, hooks, and routes
140
+ // added inside won't be visible to siblings or the root instance.
141
+ // Shared utilities (auth, db) must use fastify-plugin to break scope.
142
+ // EXAMPLE:
143
+ // WRONG:
144
+ // export async function authPlugin(fastify: FastifyInstance) {
145
+ // fastify.decorate('verifyToken', verifyToken);
146
+ // }
147
+ // RIGHT:
148
+ // import fp from 'fastify-plugin';
149
+ // export default fp(async function authPlugin(fastify: FastifyInstance) {
150
+ // fastify.decorate('verifyToken', verifyToken);
151
+ // });
152
+ // ─────────────────────────────────────────
153
+ {
154
+ id: 'fastify-use-fastify-plugin',
155
+ label: 'Shared plugins must use fastify-plugin (fp)',
156
+ description:
157
+ 'Plugins that add decorations, hooks, or services used by other plugins must be wrapped with fastify-plugin (fp) to share scope with the parent context.',
158
+ severity: 'warning',
159
+ fileExtensions: ['ts', 'js'],
160
+ pattern: null,
161
+ customCheck: (file) => {
162
+ if (
163
+ !file.relativePath.includes('/plugins/') &&
164
+ !file.relativePath.endsWith('.plugin.ts') &&
165
+ !file.relativePath.endsWith('.plugin.js')
166
+ ) {
167
+ return [];
168
+ }
169
+
170
+ const usesDecorate = /fastify\.decorate\s*\(/.test(file.content);
171
+ const usesFastifyPlugin = /fastify-plugin|fp\s*\(/.test(file.content);
172
+
173
+ if (usesDecorate && !usesFastifyPlugin) {
174
+ return [
175
+ {
176
+ line: null,
177
+ message:
178
+ 'Plugin adds decorators but is not wrapped with fastify-plugin (fp). Other plugins/routes will not see these decorations.',
179
+ },
180
+ ];
181
+ }
182
+
183
+ return [];
184
+ },
185
+ applicableTo: ['fastify'],
186
+ category: 'Architecture',
187
+ },
188
+
189
+ // ── Async Handler Patterns ────────────────────────────────────────────────
190
+
191
+ // ─────────────────────────────────────────
192
+ // RULE: fastify-async-handler
193
+ // ROLE: Enforce async handler pattern
194
+ // PURPOSE: Mixing reply.send() with async return values causes double-send
195
+ // errors in Fastify. Async handlers should return the value;
196
+ // Fastify handles serialization. Use reply.send() OR return, not both.
197
+ // EXAMPLE:
198
+ // WRONG:
199
+ // fastify.get('/users', async (req, reply) => {
200
+ // const users = await userService.findAll();
201
+ // reply.send(users); // + implicit return → double send!
202
+ // });
203
+ // RIGHT:
204
+ // fastify.get('/users', async (req, reply) => {
205
+ // return userService.findAll(); // Fastify handles send automatically
206
+ // });
207
+ // ─────────────────────────────────────────
208
+ {
209
+ id: 'fastify-async-handler',
210
+ label: 'Return values from async handlers — avoid reply.send() + return',
211
+ description:
212
+ 'In async Fastify handlers, return the response value directly. Calling reply.send() AND returning a value causes a double-send error.',
213
+ severity: 'error',
214
+ fileExtensions: ['ts', 'js'],
215
+ pattern: null,
216
+ customCheck: (file) => {
217
+ const violations: Array<{ line: number | null; message: string }> = [];
218
+
219
+ if (
220
+ !file.relativePath.includes('/routes/') &&
221
+ !file.relativePath.endsWith('.route.ts')
222
+ ) {
223
+ return [];
224
+ }
225
+
226
+ for (let i = 0; i < file.lines.length; i++) {
227
+ if (/reply\.send\s*\(/.test(file.lines[i])) {
228
+ // Check if there's a return after send on same or next line
229
+ const nearby = file.lines.slice(i, i + 3).join(' ');
230
+ if (/reply\.send[^;]*;\s*(?:return|})/.test(nearby)) {
231
+ violations.push({
232
+ line: i + 1,
233
+ message:
234
+ 'reply.send() followed by implicit return in async handler. Use: return myValue; instead of reply.send(myValue).',
235
+ });
236
+ }
237
+ }
238
+ }
239
+
240
+ return violations;
241
+ },
242
+ applicableTo: ['fastify'],
243
+ category: 'Async Patterns',
244
+ },
245
+
246
+ // ── Security ──────────────────────────────────────────────────────────────
247
+
248
+ // ─────────────────────────────────────────
249
+ // RULE: fastify-use-helmet
250
+ // ROLE: Enforce security headers
251
+ // PURPOSE: @fastify/helmet sets a baseline of secure HTTP headers.
252
+ // Without it, Fastify serves responses without X-Frame-Options,
253
+ // Content-Security-Policy, or HSTS headers.
254
+ // EXAMPLE:
255
+ // WRONG:
256
+ // const fastify = Fastify();
257
+ // fastify.register(cors);
258
+ // RIGHT:
259
+ // const fastify = Fastify();
260
+ // fastify.register(helmet);
261
+ // fastify.register(cors, { origin: allowedOrigins });
262
+ // ─────────────────────────────────────────
263
+ {
264
+ id: 'fastify-use-helmet',
265
+ label: 'Register @fastify/helmet for security headers',
266
+ description:
267
+ 'Register @fastify/helmet to add baseline security headers (HSTS, X-Frame-Options, CSP, etc.) to all responses.',
268
+ severity: 'error',
269
+ fileExtensions: ['ts', 'js'],
270
+ pattern: null,
271
+ customCheck: (file) => {
272
+ if (
273
+ !file.relativePath.endsWith('server.ts') &&
274
+ !file.relativePath.endsWith('app.ts') &&
275
+ !file.relativePath.endsWith('index.ts')
276
+ ) {
277
+ return [];
278
+ }
279
+
280
+ const hasFastify = /Fastify\s*\(|fastify\s*\(\s*\)/.test(file.content);
281
+ const hasHelmet = /helmet/.test(file.content);
282
+
283
+ if (hasFastify && !hasHelmet) {
284
+ return [
285
+ {
286
+ line: null,
287
+ message:
288
+ 'Fastify server missing @fastify/helmet. Register it: fastify.register(import("@fastify/helmet")).',
289
+ },
290
+ ];
291
+ }
292
+
293
+ return [];
294
+ },
295
+ applicableTo: ['fastify'],
296
+ category: 'Security',
297
+ },
298
+
299
+ // ─────────────────────────────────────────
300
+ // RULE: fastify-cors-options
301
+ // ROLE: Enforce CORS configuration
302
+ // PURPOSE: Registering @fastify/cors without an origin restriction allows
303
+ // cross-origin requests from any domain, defeating CORS entirely.
304
+ // EXAMPLE:
305
+ // WRONG:
306
+ // fastify.register(cors); // allows all origins
307
+ // RIGHT:
308
+ // fastify.register(cors, { origin: ['https://myapp.com'] });
309
+ // ─────────────────────────────────────────
310
+ {
311
+ id: 'fastify-cors-options',
312
+ label: '@fastify/cors must restrict origin',
313
+ description:
314
+ 'Do not register @fastify/cors without an explicit origin allowlist. An unrestricted CORS policy allows any website to make authenticated requests to your API.',
315
+ severity: 'warning',
316
+ fileExtensions: ['ts', 'js'],
317
+ pattern: /register\s*\(\s*cors\s*\)/g,
318
+ applicableTo: ['fastify'],
319
+ category: 'Security',
320
+ },
321
+ ];
322
+
323
+ // Register all Fastify-specific rules
324
+ registerRules('fastify', fastifyRules);
325
+
326
+ export { fastifyRules };
@@ -0,0 +1,331 @@
1
+ /**
2
+ * ============================================================================
3
+ * hono.config.ts — Hono-specific coding rules
4
+ * ============================================================================
5
+ *
6
+ * Registers rules that apply to Hono (edge + server) projects:
7
+ * - Zod validation enforcement
8
+ * - Context variable typing
9
+ * - Middleware chaining patterns
10
+ * - HTTPException usage
11
+ * - Environment access patterns
12
+ * - Stateless / edge-first patterns
13
+ */
14
+
15
+ import { registerRules, Rule } from './guidelines.config';
16
+
17
+ const honoRules: Rule[] = [
18
+
19
+ // ── Validation ────────────────────────────────────────────────────────────
20
+
21
+ // ─────────────────────────────────────────
22
+ // RULE: hono-use-zod-validator
23
+ // ROLE: Enforce schema-based request validation
24
+ // PURPOSE: Accessing c.req.json() or c.req.query() without validation
25
+ // passes raw untyped data to your handlers. Using @hono/zod-validator
26
+ // ensures type safety and returns a 400 automatically on bad input.
27
+ // EXAMPLE:
28
+ // WRONG:
29
+ // app.post('/users', async (c) => {
30
+ // const body = await c.req.json(); // untyped, unvalidated
31
+ // });
32
+ // RIGHT:
33
+ // import { zValidator } from '@hono/zod-validator';
34
+ // app.post('/users', zValidator('json', CreateUserSchema), async (c) => {
35
+ // const body = c.req.valid('json'); // typed and validated
36
+ // });
37
+ // ─────────────────────────────────────────
38
+ {
39
+ id: 'hono-use-zod-validator',
40
+ label: 'Use zValidator for request body validation',
41
+ description:
42
+ 'Use @hono/zod-validator middleware on routes that accept request bodies. Calling c.req.json() directly bypasses type safety and validation.',
43
+ severity: 'error',
44
+ fileExtensions: ['ts'],
45
+ pattern: null,
46
+ customCheck: (file) => {
47
+ const violations: Array<{ line: number | null; message: string }> = [];
48
+
49
+ if (
50
+ !file.relativePath.includes('/routes/') &&
51
+ !file.relativePath.endsWith('.route.ts') &&
52
+ !file.relativePath.endsWith('.routes.ts')
53
+ ) {
54
+ return [];
55
+ }
56
+
57
+ // Detect routes that use c.req.json() without zValidator
58
+ for (let i = 0; i < file.lines.length; i++) {
59
+ const line = file.lines[i];
60
+ if (/c\.req\.json\s*\(/.test(line)) {
61
+ // Check surrounding context for zValidator
62
+ const context = file.lines.slice(Math.max(0, i - 10), i).join('\n');
63
+ if (!context.includes('zValidator') && !context.includes('z.object')) {
64
+ violations.push({
65
+ line: i + 1,
66
+ message:
67
+ 'c.req.json() used without zValidator middleware. Use zValidator("json", schema) before the handler and access via c.req.valid("json").',
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ return violations;
74
+ },
75
+ applicableTo: ['hono'],
76
+ category: 'Validation',
77
+ },
78
+
79
+ // ─────────────────────────────────────────
80
+ // RULE: hono-use-valid-not-json
81
+ // ROLE: Enforce validated data access after zValidator
82
+ // PURPOSE: After applying zValidator middleware, handlers must use
83
+ // c.req.valid() to access the validated, typed data.
84
+ // c.req.json() bypasses validation and returns raw untyped data.
85
+ // EXAMPLE:
86
+ // WRONG:
87
+ // app.post('/user', zValidator('json', schema), async (c) => {
88
+ // const body = await c.req.json(); // still untyped!
89
+ // });
90
+ // RIGHT:
91
+ // app.post('/user', zValidator('json', schema), async (c) => {
92
+ // const body = c.req.valid('json'); // typed + validated
93
+ // });
94
+ // ─────────────────────────────────────────
95
+ {
96
+ id: 'hono-use-valid-not-json',
97
+ label: 'After zValidator, use c.req.valid() not c.req.json()',
98
+ description:
99
+ 'When zValidator middleware is applied, access validated data via c.req.valid("json") instead of await c.req.json(). The latter bypasses type narrowing.',
100
+ severity: 'warning',
101
+ fileExtensions: ['ts'],
102
+ pattern: null,
103
+ customCheck: (file) => {
104
+ const violations: Array<{ line: number | null; message: string }> = [];
105
+
106
+ if (!file.content.includes('zValidator')) return [];
107
+
108
+ for (let i = 0; i < file.lines.length; i++) {
109
+ if (/await c\.req\.json\s*\(/.test(file.lines[i])) {
110
+ violations.push({
111
+ line: i + 1,
112
+ message:
113
+ 'zValidator is used in this file. Replace await c.req.json() with c.req.valid("json") to access the typed validated data.',
114
+ });
115
+ }
116
+ }
117
+
118
+ return violations;
119
+ },
120
+ applicableTo: ['hono'],
121
+ category: 'Validation',
122
+ },
123
+
124
+ // ── Error Handling ────────────────────────────────────────────────────────
125
+
126
+ // ─────────────────────────────────────────
127
+ // RULE: hono-use-http-exception
128
+ // ROLE: Enforce HTTPException for expected errors
129
+ // PURPOSE: Throwing a raw Error in a Hono handler produces a generic 500
130
+ // response with no meaningful body. HTTPException allows you to set
131
+ // a specific status code and message that will be formatted by
132
+ // Hono's onError handler consistently.
133
+ // EXAMPLE:
134
+ // WRONG:
135
+ // throw new Error('User not found');
136
+ // RIGHT:
137
+ // throw new HTTPException(404, { message: 'User not found' });
138
+ // ─────────────────────────────────────────
139
+ {
140
+ id: 'hono-use-http-exception',
141
+ label: 'Use HTTPException instead of raw Error',
142
+ description:
143
+ 'Throw HTTPException from hono with a status code and message for expected errors (404, 401, 400, etc.). Raw Error objects produce unformatted 500 responses.',
144
+ severity: 'warning',
145
+ fileExtensions: ['ts'],
146
+ pattern: null,
147
+ customCheck: (file) => {
148
+ const violations: Array<{ line: number | null; message: string }> = [];
149
+
150
+ if (
151
+ !file.relativePath.includes('/routes/') &&
152
+ !file.relativePath.includes('/handlers/') &&
153
+ !file.relativePath.endsWith('.route.ts') &&
154
+ !file.relativePath.endsWith('.handler.ts')
155
+ ) {
156
+ return [];
157
+ }
158
+
159
+ for (let i = 0; i < file.lines.length; i++) {
160
+ if (/throw\s+new\s+Error\s*\(/.test(file.lines[i])) {
161
+ violations.push({
162
+ line: i + 1,
163
+ message:
164
+ 'Raw Error thrown. Use HTTPException: throw new HTTPException(404, { message: "Not found" })',
165
+ });
166
+ }
167
+ }
168
+
169
+ return violations;
170
+ },
171
+ applicableTo: ['hono'],
172
+ category: 'Error Handling',
173
+ },
174
+
175
+ // ── Context Variables ─────────────────────────────────────────────────────
176
+
177
+ // ─────────────────────────────────────────
178
+ // RULE: hono-type-context-variables
179
+ // ROLE: Enforce typed context variables
180
+ // PURPOSE: Using c.get() and c.set() without typed Variables means the
181
+ // TypeScript compiler cannot verify keys exist, leading to
182
+ // undefined value bugs at runtime.
183
+ // EXAMPLE:
184
+ // WRONG:
185
+ // app.use(async (c, next) => {
186
+ // c.set('user', user); // no type checking on 'user' key
187
+ // });
188
+ // RIGHT:
189
+ // type Variables = { user: User };
190
+ // const app = new Hono<{ Variables: Variables }>();
191
+ // app.use(async (c, next) => { c.set('user', user); });
192
+ // ─────────────────────────────────────────
193
+ {
194
+ id: 'hono-type-context-variables',
195
+ label: 'Type context Variables for c.get() / c.set()',
196
+ description:
197
+ 'Declare a typed Variables interface and pass it as a generic to new Hono<{ Variables: Variables }>(). This enables TypeScript to validate context variable keys and values.',
198
+ severity: 'warning',
199
+ fileExtensions: ['ts'],
200
+ pattern: null,
201
+ customCheck: (file) => {
202
+ const hasContextSet = /c\.set\s*\(/.test(file.content);
203
+ const hasHonoGeneric = /new\s+Hono\s*</.test(file.content);
204
+ const hasVariablesType = /Variables\s*=\s*\{/.test(file.content);
205
+
206
+ if (hasContextSet && !hasHonoGeneric && !hasVariablesType) {
207
+ return [
208
+ {
209
+ line: null,
210
+ message:
211
+ 'c.set() is used without typed Variables. Define: type Variables = { ... } and use new Hono<{ Variables: Variables }>().',
212
+ },
213
+ ];
214
+ }
215
+
216
+ return [];
217
+ },
218
+ applicableTo: ['hono'],
219
+ category: 'Type Safety',
220
+ },
221
+
222
+ // ── Environment Access ────────────────────────────────────────────────────
223
+
224
+ // ─────────────────────────────────────────
225
+ // RULE: hono-use-c-env
226
+ // ROLE: Enforce correct env access on edge runtimes
227
+ // PURPOSE: On Cloudflare Workers and similar runtimes, process.env is
228
+ // unavailable. Environment bindings are accessed via c.env.
229
+ // Even on Node.js, centralising env access via c.env makes
230
+ // the app portable across runtimes.
231
+ // EXAMPLE:
232
+ // WRONG:
233
+ // const secret = process.env['JWT_SECRET'];
234
+ // RIGHT:
235
+ // // In Hono handler:
236
+ // const secret = c.env.JWT_SECRET;
237
+ // ─────────────────────────────────────────
238
+ {
239
+ id: 'hono-use-c-env',
240
+ label: 'Use c.env for environment variables (edge-compatible)',
241
+ description:
242
+ 'Prefer c.env over process.env for environment variables. process.env is not available in Cloudflare Workers or Deno and makes apps runtime-specific.',
243
+ severity: 'warning',
244
+ fileExtensions: ['ts'],
245
+ pattern: null,
246
+ customCheck: (file) => {
247
+ const violations: Array<{ line: number | null; message: string }> = [];
248
+
249
+ if (
250
+ !file.relativePath.includes('/routes/') &&
251
+ !file.relativePath.includes('/handlers/') &&
252
+ !file.relativePath.endsWith('.route.ts') &&
253
+ !file.relativePath.endsWith('.handler.ts') &&
254
+ !file.relativePath.endsWith('.middleware.ts')
255
+ ) {
256
+ return [];
257
+ }
258
+
259
+ for (let i = 0; i < file.lines.length; i++) {
260
+ if (/process\.env\b/.test(file.lines[i])) {
261
+ violations.push({
262
+ line: i + 1,
263
+ message:
264
+ 'process.env used in Hono handler/route. Use c.env.VARIABLE_NAME for edge-runtime portability.',
265
+ });
266
+ }
267
+ }
268
+
269
+ return violations;
270
+ },
271
+ applicableTo: ['hono'],
272
+ category: 'Security',
273
+ },
274
+
275
+ // ── Route Organisation ────────────────────────────────────────────────────
276
+
277
+ // ─────────────────────────────────────────
278
+ // RULE: hono-use-sub-apps
279
+ // ROLE: Enforce composable route architecture
280
+ // PURPOSE: Defining all routes on a single root Hono instance leads to a
281
+ // monolithic file. Sub-apps (separate Hono instances per domain)
282
+ // composed via app.route() enable feature isolation and per-domain
283
+ // middleware application.
284
+ // EXAMPLE:
285
+ // WRONG:
286
+ // app.get('/users', ...)
287
+ // app.post('/users', ...)
288
+ // app.get('/products', ...) // all in one file
289
+ // RIGHT:
290
+ // // users.route.ts
291
+ // const users = new Hono();
292
+ // users.get('/', ...)
293
+ // export default users;
294
+ // // app.ts
295
+ // app.route('/users', users);
296
+ // ─────────────────────────────────────────
297
+ {
298
+ id: 'hono-use-sub-apps',
299
+ label: 'Organise routes as Hono sub-apps using app.route()',
300
+ description:
301
+ 'Define each domain\'s routes in a separate Hono sub-app and compose them via app.route("/prefix", subApp). Avoid putting all routes on a single root instance.',
302
+ severity: 'info',
303
+ fileExtensions: ['ts'],
304
+ pattern: null,
305
+ customCheck: (file) => {
306
+ if (!file.relativePath.endsWith('index.ts') && !file.relativePath.endsWith('app.ts')) {
307
+ return [];
308
+ }
309
+
310
+ const routeCount = (file.content.match(/app\.\s*(?:get|post|put|patch|delete)\s*\(/g) || []).length;
311
+
312
+ if (routeCount > 5) {
313
+ return [
314
+ {
315
+ line: null,
316
+ message: `${routeCount} routes defined on a single Hono instance. Extract into domain sub-apps and use app.route('/prefix', subApp).`,
317
+ },
318
+ ];
319
+ }
320
+
321
+ return [];
322
+ },
323
+ applicableTo: ['hono'],
324
+ category: 'Architecture',
325
+ },
326
+ ];
327
+
328
+ // Register all Hono-specific rules
329
+ registerRules('hono', honoRules);
330
+
331
+ export { honoRules };