@agjs/tsforge 0.1.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 (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,176 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ collectElysiaVariables,
7
+ findEnclosingRouteHandler,
8
+ findObjectProperty,
9
+ isElysiaRouteCall,
10
+ } from "../utils/elysiaChain";
11
+
12
+ export const RULE_NAME = "prefer-direct-return";
13
+
14
+ export interface PreferDirectReturnOptions {
15
+ readonly allowWithHeaders?: boolean;
16
+ }
17
+
18
+ type RuleOptions = [PreferDirectReturnOptions];
19
+ type MessageIds = "preferDirectReturn";
20
+
21
+ const optionSchema: JSONSchema4 = {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ allowWithHeaders: { type: "boolean" },
26
+ },
27
+ };
28
+
29
+ export const preferDirectReturnRule = createRule<RuleOptions, MessageIds>({
30
+ name: RULE_NAME,
31
+ meta: {
32
+ type: "suggestion",
33
+ docs: {
34
+ description:
35
+ "Inside Elysia route handlers, return values directly instead of wrapping them in `new Response(...)` or `Response.json(...)` — Elysia handles serialization and content-type automatically.",
36
+ },
37
+ schema: [optionSchema],
38
+ messages: {
39
+ preferDirectReturn:
40
+ "Return the value directly — Elysia will serialize it and set the correct content-type. Wrapping in `new Response(...)` is only necessary for streams or custom headers.",
41
+ },
42
+ },
43
+ defaultOptions: [{ allowWithHeaders: true }],
44
+ create(context, [options]) {
45
+ const allowWithHeaders = options.allowWithHeaders !== false;
46
+ let elysiaVars = new Set<string>();
47
+
48
+ return {
49
+ Program(program) {
50
+ elysiaVars = collectElysiaVariables(program);
51
+ },
52
+ ReturnStatement(node) {
53
+ if (!node.argument) {
54
+ return;
55
+ }
56
+
57
+ if (!findEnclosingRouteHandler(node, elysiaVars)) {
58
+ return;
59
+ }
60
+
61
+ checkExpression(node.argument);
62
+ },
63
+ ArrowFunctionExpression(node) {
64
+ if (node.body.type === AST_NODE_TYPES.BlockStatement) {
65
+ return;
66
+ }
67
+
68
+ if (!isRouteHandlerArg(node, elysiaVars)) {
69
+ return;
70
+ }
71
+
72
+ checkExpression(node.body);
73
+ },
74
+ };
75
+
76
+ function checkExpression(arg: TSESTree.Expression): void {
77
+ if (arg.type === AST_NODE_TYPES.NewExpression) {
78
+ if (
79
+ arg.callee.type !== AST_NODE_TYPES.Identifier ||
80
+ arg.callee.name !== "Response"
81
+ ) {
82
+ return;
83
+ }
84
+
85
+ const bodyArg = arg.arguments[0];
86
+
87
+ if (!bodyArg || !isSimpleBody(bodyArg)) {
88
+ return;
89
+ }
90
+
91
+ const optionsArg = arg.arguments[1];
92
+
93
+ if (
94
+ allowWithHeaders &&
95
+ optionsArg &&
96
+ hasHeadersOrContentType(optionsArg)
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ context.report({ node: arg, messageId: "preferDirectReturn" });
102
+
103
+ return;
104
+ }
105
+
106
+ if (
107
+ arg.type === AST_NODE_TYPES.CallExpression &&
108
+ arg.callee.type === AST_NODE_TYPES.MemberExpression &&
109
+ arg.callee.object.type === AST_NODE_TYPES.Identifier &&
110
+ arg.callee.object.name === "Response" &&
111
+ arg.callee.property.type === AST_NODE_TYPES.Identifier &&
112
+ arg.callee.property.name === "json"
113
+ ) {
114
+ context.report({ node: arg, messageId: "preferDirectReturn" });
115
+ }
116
+ }
117
+ },
118
+ });
119
+
120
+ function isRouteHandlerArg(
121
+ fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
122
+ elysiaVars: Set<string>
123
+ ): boolean {
124
+ const parent = fn.parent;
125
+
126
+ if (parent?.type !== AST_NODE_TYPES.CallExpression) {
127
+ return false;
128
+ }
129
+
130
+ if (!parent.arguments.includes(fn)) {
131
+ return false;
132
+ }
133
+
134
+ return isElysiaRouteCall(parent, elysiaVars);
135
+ }
136
+
137
+ function isSimpleBody(node: TSESTree.Node): boolean {
138
+ if (node.type === AST_NODE_TYPES.SpreadElement) {
139
+ return false;
140
+ }
141
+
142
+ if (
143
+ node.type === AST_NODE_TYPES.Literal ||
144
+ node.type === AST_NODE_TYPES.TemplateLiteral
145
+ ) {
146
+ return true;
147
+ }
148
+
149
+ if (
150
+ node.type === AST_NODE_TYPES.CallExpression &&
151
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
152
+ node.callee.object.type === AST_NODE_TYPES.Identifier &&
153
+ node.callee.object.name === "JSON" &&
154
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
155
+ node.callee.property.name === "stringify"
156
+ ) {
157
+ return true;
158
+ }
159
+
160
+ if (
161
+ node.type === AST_NODE_TYPES.ObjectExpression ||
162
+ node.type === AST_NODE_TYPES.ArrayExpression
163
+ ) {
164
+ return true;
165
+ }
166
+
167
+ return false;
168
+ }
169
+
170
+ function hasHeadersOrContentType(opts: TSESTree.Node): boolean {
171
+ if (opts.type !== AST_NODE_TYPES.ObjectExpression) {
172
+ return false;
173
+ }
174
+
175
+ return findObjectProperty(opts, "headers") !== null;
176
+ }
@@ -0,0 +1,159 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import { walkSome } from "../../utils";
6
+ import {
7
+ collectElysiaVariables,
8
+ findEnclosingRouteHandler,
9
+ } from "../utils/elysiaChain";
10
+
11
+ export const RULE_NAME = "prefer-static-services";
12
+
13
+ export interface PreferStaticServicesOptions {
14
+ readonly classNamePattern?: string;
15
+ }
16
+
17
+ type RuleOptions = [PreferStaticServicesOptions];
18
+ type MessageIds = "preferStaticService";
19
+
20
+ const DEFAULT_PATTERN = "(Service|Controller|Manager|Repository)$";
21
+
22
+ const optionSchema: JSONSchema4 = {
23
+ type: "object",
24
+ additionalProperties: false,
25
+ properties: {
26
+ classNamePattern: { type: "string", minLength: 1 },
27
+ },
28
+ };
29
+
30
+ export const preferStaticServicesRule = createRule<RuleOptions, MessageIds>({
31
+ name: RULE_NAME,
32
+ meta: {
33
+ type: "suggestion",
34
+ docs: {
35
+ description:
36
+ "Discourage `new Service()` inside Elysia route handlers when the class is stateless — prefer static methods or a singleton.",
37
+ },
38
+ schema: [optionSchema],
39
+ messages: {
40
+ preferStaticService:
41
+ "Avoid `new {{name}}()` inside an Elysia route handler — '{{name}}' has no instance state, so allocating per request is wasteful. Use static methods or a module-level singleton.",
42
+ },
43
+ },
44
+ defaultOptions: [{ classNamePattern: DEFAULT_PATTERN }],
45
+ create(context, [options]) {
46
+ const pattern = compilePattern(options.classNamePattern ?? DEFAULT_PATTERN);
47
+
48
+ if (!pattern) {
49
+ return {};
50
+ }
51
+
52
+ let elysiaVars = new Set<string>();
53
+ const classes = new Map<string, TSESTree.ClassDeclaration>();
54
+ const newExpressions: TSESTree.NewExpression[] = [];
55
+
56
+ return {
57
+ Program(program) {
58
+ elysiaVars = collectElysiaVariables(program);
59
+ },
60
+ ClassDeclaration(node) {
61
+ if (node.id) {
62
+ classes.set(node.id.name, node);
63
+ }
64
+ },
65
+ NewExpression(node) {
66
+ if (node.callee.type !== AST_NODE_TYPES.Identifier) {
67
+ return;
68
+ }
69
+
70
+ if (!pattern.test(node.callee.name)) {
71
+ return;
72
+ }
73
+
74
+ newExpressions.push(node);
75
+ },
76
+ "Program:exit"() {
77
+ for (const newExpr of newExpressions) {
78
+ if (newExpr.callee.type !== AST_NODE_TYPES.Identifier) {
79
+ continue;
80
+ }
81
+
82
+ const className = newExpr.callee.name;
83
+ const classDecl = classes.get(className);
84
+
85
+ if (!classDecl || !isStateless(classDecl)) {
86
+ continue;
87
+ }
88
+
89
+ if (!findEnclosingRouteHandler(newExpr, elysiaVars)) {
90
+ continue;
91
+ }
92
+
93
+ context.report({
94
+ node: newExpr,
95
+ messageId: "preferStaticService",
96
+ data: { name: className },
97
+ });
98
+ }
99
+ },
100
+ };
101
+ },
102
+ });
103
+
104
+ function isStateless(node: TSESTree.ClassDeclaration): boolean {
105
+ return node.body.body.every(memberIsStateless);
106
+ }
107
+
108
+ function memberIsStateless(member: TSESTree.ClassElement): boolean {
109
+ if (member.type === AST_NODE_TYPES.PropertyDefinition) {
110
+ return member.static;
111
+ }
112
+
113
+ if (member.type !== AST_NODE_TYPES.MethodDefinition) {
114
+ return true;
115
+ }
116
+
117
+ if (member.kind === "constructor" && constructorHasState(member.value)) {
118
+ return false;
119
+ }
120
+
121
+ return (
122
+ member.static ||
123
+ member.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression ||
124
+ !assignsToThis(member.value)
125
+ );
126
+ }
127
+
128
+ function constructorHasState(
129
+ ctor: TSESTree.FunctionExpression | TSESTree.TSEmptyBodyFunctionExpression
130
+ ): boolean {
131
+ if (
132
+ ctor.body?.type === AST_NODE_TYPES.BlockStatement &&
133
+ ctor.body.body.length > 0
134
+ ) {
135
+ return true;
136
+ }
137
+
138
+ return ctor.params.some(
139
+ (param) => param.type === AST_NODE_TYPES.TSParameterProperty
140
+ );
141
+ }
142
+
143
+ function assignsToThis(fn: TSESTree.FunctionExpression): boolean {
144
+ return walkSome(
145
+ fn.body,
146
+ (node) =>
147
+ node.type === AST_NODE_TYPES.AssignmentExpression &&
148
+ node.left.type === AST_NODE_TYPES.MemberExpression &&
149
+ node.left.object.type === AST_NODE_TYPES.ThisExpression
150
+ );
151
+ }
152
+
153
+ function compilePattern(source: string): RegExp | null {
154
+ try {
155
+ return new RegExp(source);
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
@@ -0,0 +1,151 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ collectElysiaVariables,
7
+ findEnclosingRouteHandler,
8
+ } from "../utils/elysiaChain";
9
+
10
+ export const RULE_NAME = "prefer-throw-status";
11
+
12
+ export interface PreferThrowStatusOptions {
13
+ readonly maxStatements?: number;
14
+ }
15
+
16
+ type RuleOptions = [PreferThrowStatusOptions];
17
+ type MessageIds = "preferThrowStatus";
18
+
19
+ const DEFAULT_MAX_STATEMENTS = 3;
20
+
21
+ const optionSchema: JSONSchema4 = {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ maxStatements: { type: "integer", minimum: 1 },
26
+ },
27
+ };
28
+
29
+ export const preferThrowStatusRule = createRule<RuleOptions, MessageIds>({
30
+ name: RULE_NAME,
31
+ meta: {
32
+ type: "suggestion",
33
+ docs: {
34
+ description:
35
+ "Inside Elysia route handlers, prefer `throw status(...)` over try/catch blocks that build their own Response — local catches bypass Elysia's typed onError pipeline.",
36
+ },
37
+ schema: [optionSchema],
38
+ messages: {
39
+ preferThrowStatus:
40
+ "Avoid wrapping route logic in a try/catch that builds its own response. Use `throw status(code, message)` (or your project's typed exception) so Elysia's onError pipeline can render a typed response.",
41
+ },
42
+ },
43
+ defaultOptions: [{ maxStatements: DEFAULT_MAX_STATEMENTS }],
44
+ create(context, [options]) {
45
+ const maxStatements = options.maxStatements ?? DEFAULT_MAX_STATEMENTS;
46
+ let elysiaVars = new Set<string>();
47
+
48
+ return {
49
+ Program(program) {
50
+ elysiaVars = collectElysiaVariables(program);
51
+ },
52
+ TryStatement(node) {
53
+ if (!findEnclosingRouteHandler(node, elysiaVars)) {
54
+ return;
55
+ }
56
+
57
+ const tooLargeTry = node.block.body.length > maxStatements;
58
+ const catchProducesResponse = node.handler
59
+ ? catchBuildsResponse(node.handler.body)
60
+ : false;
61
+
62
+ if (!tooLargeTry && !catchProducesResponse) {
63
+ return;
64
+ }
65
+
66
+ context.report({ node, messageId: "preferThrowStatus" });
67
+ },
68
+ };
69
+ },
70
+ });
71
+
72
+ function catchBuildsResponse(block: TSESTree.BlockStatement): boolean {
73
+ for (const stmt of block.body) {
74
+ if (statementBuildsResponse(stmt)) {
75
+ return true;
76
+ }
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ function statementBuildsResponse(stmt: TSESTree.Statement): boolean {
83
+ if (stmt.type === AST_NODE_TYPES.ReturnStatement && stmt.argument) {
84
+ return isResponseLikeExpression(stmt.argument);
85
+ }
86
+
87
+ if (stmt.type === AST_NODE_TYPES.ExpressionStatement) {
88
+ const expr = stmt.expression;
89
+
90
+ if (
91
+ expr.type === AST_NODE_TYPES.AssignmentExpression &&
92
+ expr.left.type === AST_NODE_TYPES.MemberExpression &&
93
+ expr.left.object.type === AST_NODE_TYPES.Identifier &&
94
+ expr.left.object.name === "set" &&
95
+ expr.left.property.type === AST_NODE_TYPES.Identifier &&
96
+ expr.left.property.name === "status"
97
+ ) {
98
+ return true;
99
+ }
100
+ }
101
+
102
+ if (stmt.type === AST_NODE_TYPES.IfStatement) {
103
+ if (statementBuildsResponse(stmt.consequent)) {
104
+ return true;
105
+ }
106
+
107
+ if (stmt.alternate && statementBuildsResponse(stmt.alternate)) {
108
+ return true;
109
+ }
110
+ }
111
+
112
+ if (stmt.type === AST_NODE_TYPES.BlockStatement) {
113
+ for (const inner of stmt.body) {
114
+ if (statementBuildsResponse(inner)) {
115
+ return true;
116
+ }
117
+ }
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ function isResponseLikeExpression(node: TSESTree.Expression): boolean {
124
+ if (node.type === AST_NODE_TYPES.NewExpression) {
125
+ return (
126
+ node.callee.type === AST_NODE_TYPES.Identifier &&
127
+ node.callee.name === "Response"
128
+ );
129
+ }
130
+
131
+ if (
132
+ node.type === AST_NODE_TYPES.CallExpression &&
133
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
134
+ node.callee.object.type === AST_NODE_TYPES.Identifier &&
135
+ node.callee.object.name === "Response" &&
136
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
137
+ node.callee.property.name === "json"
138
+ ) {
139
+ return true;
140
+ }
141
+
142
+ if (
143
+ node.type === AST_NODE_TYPES.Literal ||
144
+ node.type === AST_NODE_TYPES.ObjectExpression ||
145
+ node.type === AST_NODE_TYPES.ArrayExpression
146
+ ) {
147
+ return true;
148
+ }
149
+
150
+ return false;
151
+ }
@@ -0,0 +1,209 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ HOOK_METHODS,
7
+ ROUTE_METHODS,
8
+ collectElysiaVariables,
9
+ getChainRoot,
10
+ getMemberMethodName,
11
+ isElysiaRouted,
12
+ isNewElysiaExpression,
13
+ } from "../utils/elysiaChain";
14
+
15
+ export const RULE_NAME = "require-hooks-before-routes";
16
+
17
+ export interface RequireHooksBeforeRoutesOptions {
18
+ readonly hooks?: readonly string[];
19
+ readonly routes?: readonly string[];
20
+ }
21
+
22
+ type RuleOptions = [RequireHooksBeforeRoutesOptions];
23
+ type MessageIds = "hookAfterRoute";
24
+
25
+ const optionSchema: JSONSchema4 = {
26
+ type: "object",
27
+ additionalProperties: false,
28
+ properties: {
29
+ hooks: {
30
+ type: "array",
31
+ items: { type: "string" },
32
+ uniqueItems: true,
33
+ },
34
+ routes: {
35
+ type: "array",
36
+ items: { type: "string" },
37
+ uniqueItems: true,
38
+ },
39
+ },
40
+ };
41
+
42
+ export const requireHooksBeforeRoutesRule = createRule<RuleOptions, MessageIds>(
43
+ {
44
+ name: RULE_NAME,
45
+ meta: {
46
+ type: "problem",
47
+ docs: {
48
+ description:
49
+ "Elysia hooks (onError, onBeforeHandle, etc.) must register before any route methods on the same instance — top-down waterfall semantics mean a hook registered after a route does not apply to it.",
50
+ },
51
+ schema: [optionSchema],
52
+ messages: {
53
+ hookAfterRoute:
54
+ "Hook `.{{hook}}(...)` registered after route `.{{route}}(...)` — the hook will NOT apply to that earlier route. Move global hooks before any route on the same instance.",
55
+ },
56
+ },
57
+ defaultOptions: [
58
+ {
59
+ hooks: [...HOOK_METHODS],
60
+ routes: [...ROUTE_METHODS],
61
+ },
62
+ ],
63
+ create(context, [options]) {
64
+ const hooks = new Set(options.hooks ?? HOOK_METHODS);
65
+ const routes = new Set(options.routes ?? ROUTE_METHODS);
66
+
67
+ let elysiaVars = new Set<string>();
68
+ const flatBuilderCalls = new Map<string, OrderedCall[]>();
69
+ const reportedNodes = new WeakSet();
70
+
71
+ function reportFlatBuilders(): void {
72
+ for (const calls of flatBuilderCalls.values()) {
73
+ const firstRouteIndex = calls.findIndex((c) => routes.has(c.method));
74
+
75
+ if (firstRouteIndex === -1) {
76
+ continue;
77
+ }
78
+
79
+ const firstRoute = calls[firstRouteIndex];
80
+
81
+ for (let i = firstRouteIndex + 1; i < calls.length; i++) {
82
+ const call = calls[i];
83
+
84
+ if (!call) {
85
+ continue;
86
+ }
87
+
88
+ if (
89
+ hooks.has(call.method) &&
90
+ firstRoute &&
91
+ !reportedNodes.has(call.node)
92
+ ) {
93
+ reportedNodes.add(call.node);
94
+ context.report({
95
+ node: call.node,
96
+ messageId: "hookAfterRoute",
97
+ data: { hook: call.method, route: firstRoute.method },
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ function reportChain(rootCall: TSESTree.CallExpression): void {
105
+ const ordered = collectChainCalls(rootCall);
106
+
107
+ const firstRouteIndex = ordered.findIndex((c) => routes.has(c.method));
108
+
109
+ if (firstRouteIndex === -1) {
110
+ return;
111
+ }
112
+
113
+ const firstRoute = ordered[firstRouteIndex];
114
+
115
+ for (let i = firstRouteIndex + 1; i < ordered.length; i++) {
116
+ const call = ordered[i];
117
+
118
+ if (!call) {
119
+ continue;
120
+ }
121
+
122
+ if (
123
+ hooks.has(call.method) &&
124
+ firstRoute &&
125
+ !reportedNodes.has(call.node)
126
+ ) {
127
+ reportedNodes.add(call.node);
128
+ context.report({
129
+ node: call.node,
130
+ messageId: "hookAfterRoute",
131
+ data: { hook: call.method, route: firstRoute.method },
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ return {
138
+ Program(program) {
139
+ elysiaVars = collectElysiaVariables(program);
140
+ },
141
+ CallExpression(node) {
142
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
143
+ return;
144
+ }
145
+
146
+ const method = getMemberMethodName(node);
147
+
148
+ if (!method) {
149
+ return;
150
+ }
151
+
152
+ if (!isElysiaRouted(node, elysiaVars)) {
153
+ return;
154
+ }
155
+
156
+ const root = getChainRoot(node);
157
+
158
+ if (isNewElysiaExpression(root)) {
159
+ if (
160
+ node.parent?.type === AST_NODE_TYPES.MemberExpression &&
161
+ node.parent.parent?.type === AST_NODE_TYPES.CallExpression &&
162
+ isElysiaRouted(node.parent.parent, elysiaVars)
163
+ ) {
164
+ return;
165
+ }
166
+
167
+ reportChain(node);
168
+
169
+ return;
170
+ }
171
+
172
+ if (
173
+ root.type === AST_NODE_TYPES.Identifier &&
174
+ elysiaVars.has(root.name)
175
+ ) {
176
+ const list = flatBuilderCalls.get(root.name) ?? [];
177
+
178
+ list.push({ method, node });
179
+ flatBuilderCalls.set(root.name, list);
180
+ }
181
+ },
182
+ "Program:exit"() {
183
+ reportFlatBuilders();
184
+ },
185
+ };
186
+ },
187
+ }
188
+ );
189
+
190
+ interface OrderedCall {
191
+ readonly method: string;
192
+ readonly node: TSESTree.CallExpression;
193
+ }
194
+
195
+ function collectChainCalls(outermost: TSESTree.CallExpression): OrderedCall[] {
196
+ const calls: OrderedCall[] = [];
197
+ let current: TSESTree.Expression = outermost;
198
+
199
+ while (
200
+ current.type === AST_NODE_TYPES.CallExpression &&
201
+ current.callee.type === AST_NODE_TYPES.MemberExpression &&
202
+ current.callee.property.type === AST_NODE_TYPES.Identifier
203
+ ) {
204
+ calls.push({ method: current.callee.property.name, node: current });
205
+ current = current.callee.object;
206
+ }
207
+
208
+ return calls.reverse();
209
+ }