create-backbone-template 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 (182) hide show
  1. package/README.md +33 -0
  2. package/bin/create-backbone-template.js +5 -0
  3. package/package.json +30 -0
  4. package/src/create-backbone-template.js +204 -0
  5. package/template/.agents/skills/agent-browser/SKILL.md +55 -0
  6. package/template/.agents/skills/create-plan/SKILL.md +52 -0
  7. package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
  8. package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
  9. package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
  10. package/template/.agents/skills/implement-plan/SKILL.md +26 -0
  11. package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
  12. package/template/.agents/skills/review-plan/SKILL.md +38 -0
  13. package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
  14. package/template/.env.schema +30 -0
  15. package/template/.env.test +6 -0
  16. package/template/.oxlintrc.json +67 -0
  17. package/template/.vscode/extensions.json +3 -0
  18. package/template/.vscode/settings.json +23 -0
  19. package/template/AGENTS.md +55 -0
  20. package/template/Cargo.lock +2648 -0
  21. package/template/Cargo.toml +29 -0
  22. package/template/Justfile +140 -0
  23. package/template/README.md +72 -0
  24. package/template/TODO.md +1 -0
  25. package/template/_gitignore +12 -0
  26. package/template/buf.gen.yaml +7 -0
  27. package/template/buf.yaml +10 -0
  28. package/template/client/.oxfmtrc.json +8 -0
  29. package/template/client/.oxlintrc.json +57 -0
  30. package/template/client/README.md +19 -0
  31. package/template/client/_gitignore +5 -0
  32. package/template/client/index.html +12 -0
  33. package/template/client/package.json +47 -0
  34. package/template/client/packages/design-system/package.json +19 -0
  35. package/template/client/packages/design-system/src/index.ts +2 -0
  36. package/template/client/packages/design-system-basic/package.json +18 -0
  37. package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
  38. package/template/client/packages/design-system-basic/src/button.tsx +26 -0
  39. package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
  40. package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
  41. package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
  42. package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
  43. package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
  44. package/template/client/packages/design-system-basic/src/form.tsx +9 -0
  45. package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
  46. package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
  47. package/template/client/packages/design-system-basic/src/index.tsx +15 -0
  48. package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
  49. package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
  50. package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
  51. package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
  52. package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
  53. package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
  54. package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
  55. package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
  56. package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
  57. package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
  58. package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
  59. package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
  60. package/template/client/packages/design-system-basic/src/styles.css +254 -0
  61. package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
  62. package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
  63. package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
  64. package/template/client/packages/design-system-basic/src/text.tsx +5 -0
  65. package/template/client/packages/design-system-contract/package.json +15 -0
  66. package/template/client/packages/design-system-contract/src/button.ts +10 -0
  67. package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
  68. package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
  69. package/template/client/packages/design-system-contract/src/form.ts +9 -0
  70. package/template/client/packages/design-system-contract/src/heading.ts +9 -0
  71. package/template/client/packages/design-system-contract/src/index.ts +13 -0
  72. package/template/client/packages/design-system-contract/src/inline.ts +7 -0
  73. package/template/client/packages/design-system-contract/src/layout.ts +8 -0
  74. package/template/client/packages/design-system-contract/src/loader.ts +7 -0
  75. package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
  76. package/template/client/packages/design-system-contract/src/notice.ts +8 -0
  77. package/template/client/packages/design-system-contract/src/stack.ts +8 -0
  78. package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
  79. package/template/client/packages/design-system-contract/src/text.ts +9 -0
  80. package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
  81. package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
  82. package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
  83. package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
  84. package/template/client/packages/design-system-lint/package.json +23 -0
  85. package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
  86. package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
  87. package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
  88. package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
  89. package/template/client/packages/design-system-lint/src/rules.ts +111 -0
  90. package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
  91. package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
  92. package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
  93. package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
  94. package/template/client/packages/design-system-lint/tsconfig.json +29 -0
  95. package/template/client/src/App.tsx +77 -0
  96. package/template/client/src/design-system-components.test.tsx +75 -0
  97. package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
  98. package/template/client/src/main.tsx +18 -0
  99. package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
  100. package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
  101. package/template/client/src/pages/hello/hello-page.tsx +126 -0
  102. package/template/client/src/pages/page.ts +20 -0
  103. package/template/client/src/testing/create-preview-events.test.ts +36 -0
  104. package/template/client/src/testing/create-preview-events.ts +30 -0
  105. package/template/client/src/vite-env.d.ts +1 -0
  106. package/template/client/tsconfig.json +32 -0
  107. package/template/client/vite.config.ts +21 -0
  108. package/template/client/vite.ladle.config.ts +5 -0
  109. package/template/e2e/.gherkin-lintrc +20 -0
  110. package/template/e2e/.oxfmtrc.json +15 -0
  111. package/template/e2e/.oxlintrc.json +37 -0
  112. package/template/e2e/_gitignore +4 -0
  113. package/template/e2e/features/helloworld.feature +10 -0
  114. package/template/e2e/package.json +42 -0
  115. package/template/e2e/playwright.config.ts +16 -0
  116. package/template/e2e/support/app-gherkin.ts +4 -0
  117. package/template/e2e/support/fixtures.ts +236 -0
  118. package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
  119. package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
  120. package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
  121. package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
  122. package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
  123. package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
  124. package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
  125. package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
  126. package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
  127. package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
  128. package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
  129. package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
  130. package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
  131. package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
  132. package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
  133. package/template/e2e/support/gherkin.test.ts +184 -0
  134. package/template/e2e/support/gherkin.ts +321 -0
  135. package/template/e2e/support/oxlint-plugin.test.ts +328 -0
  136. package/template/e2e/support/oxlint-plugin.ts +485 -0
  137. package/template/e2e/tests/helloworld.spec.ts +39 -0
  138. package/template/e2e/tsconfig.json +26 -0
  139. package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
  140. package/template/package.json +9 -0
  141. package/template/pnpm-lock.yaml +10723 -0
  142. package/template/pnpm-workspace.yaml +8 -0
  143. package/template/pr-slide/README.md +95 -0
  144. package/template/pr-slide/package.json +23 -0
  145. package/template/pr-slide/src/cli.js +262 -0
  146. package/template/pr-slide/src/generate-pr-deck.js +833 -0
  147. package/template/pr-slide/src/git-context.js +91 -0
  148. package/template/pr-slide/src/presentation-paths.js +9 -0
  149. package/template/pr-slide/src/presentations.js +53 -0
  150. package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
  151. package/template/pr-slide/test/presentation-paths.test.js +14 -0
  152. package/template/pr-slide/test/presentations.test.js +50 -0
  153. package/template/proto/helloworld/v1/helloworld.proto +15 -0
  154. package/template/scripts/run-e2e.sh +10 -0
  155. package/template/server/Cargo.toml +26 -0
  156. package/template/server/build.rs +9 -0
  157. package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
  158. package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
  159. package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
  160. package/template/server/dylint/backbone_server_lints/README.md +5 -0
  161. package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
  162. package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
  163. package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
  164. package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
  165. package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
  166. package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
  167. package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
  168. package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
  169. package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
  170. package/template/server/migrations/20260520120000_create_projects.sql +12 -0
  171. package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
  172. package/template/server/src/config.rs +27 -0
  173. package/template/server/src/db/hello_world.rs +34 -0
  174. package/template/server/src/db/hello_world_tests.rs +11 -0
  175. package/template/server/src/db/mod.rs +39 -0
  176. package/template/server/src/lib.rs +10 -0
  177. package/template/server/src/main.rs +43 -0
  178. package/template/server/src/rpc/greeter/mod.rs +31 -0
  179. package/template/server/src/rpc/greeter/say_hello.rs +27 -0
  180. package/template/server/src/rpc/mod.rs +8 -0
  181. package/template/server/src/state.rs +13 -0
  182. package/template/skills-lock.json +11 -0
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "backbone_server_lints"
3
+ version = "0.1.0"
4
+ authors = ["authors go here"]
5
+ description = "description goes here"
6
+ edition = "2024"
7
+ publish = false
8
+
9
+ [workspace]
10
+
11
+ [lib]
12
+ crate-type = ["cdylib"]
13
+
14
+ [dependencies]
15
+ dylint_linting = "6.0.0"
16
+
17
+ [dev-dependencies]
18
+ dylint_testing = "6.0.0"
19
+
20
+ [package.metadata.rust-analyzer]
21
+ rustc_private = true
@@ -0,0 +1,5 @@
1
+ # Backbone Server Lints
2
+
3
+ Custom Dylint rules for the Rust server architecture. The crate is built
4
+ explicitly by `cargo dylint` through the root workspace metadata, but its own
5
+ empty `[workspace]` table keeps it out of regular server Cargo builds.
@@ -0,0 +1,3 @@
1
+ [toolchain]
2
+ channel = "nightly-2026-04-16"
3
+ components = ["llvm-tools-preview", "rustc-dev"]
@@ -0,0 +1,612 @@
1
+ #![feature(rustc_private)]
2
+
3
+ extern crate rustc_hir;
4
+ extern crate rustc_errors;
5
+ extern crate rustc_lint;
6
+ extern crate rustc_session;
7
+ extern crate rustc_span;
8
+
9
+ use rustc_errors::{DiagDecorator, DiagMessage, MultiSpan};
10
+ use rustc_hir::{Expr, ExprKind, ImplItemKind, Item, ItemKind, QPath};
11
+ use rustc_lint::{LateContext, LateLintPass, Lint, LintContext, LintStore};
12
+ use rustc_session::{Session, declare_lint, impl_lint_pass};
13
+ use rustc_span::{FileName, Span};
14
+ use std::collections::HashSet;
15
+ use std::path::{Component, Path};
16
+
17
+ const MAX_FILE_LINES: usize = 300;
18
+
19
+ dylint_linting::dylint_library!();
20
+
21
+ declare_lint! {
22
+ /// ### What it does
23
+ ///
24
+ /// Warns when a Rust source file grows beyond the server file length limit.
25
+ ///
26
+ /// ### Why is this bad?
27
+ ///
28
+ /// Long files tend to hide unrelated responsibilities and make the server
29
+ /// architecture harder to scan.
30
+ pub MAX_FILE_LENGTH,
31
+ Warn,
32
+ "server Rust files should stay below the configured maximum line count"
33
+ }
34
+
35
+ declare_lint! {
36
+ /// ### What it does
37
+ ///
38
+ /// Warns when `sqlx` is used outside a `db` source directory.
39
+ ///
40
+ /// ### Why is this bad?
41
+ ///
42
+ /// Database access should be isolated behind the server's database module.
43
+ pub SQLX_ONLY_IN_DB_FOLDER,
44
+ Deny,
45
+ "sqlx calls and imports are only allowed in db modules"
46
+ }
47
+
48
+ declare_lint! {
49
+ /// ### What it does
50
+ ///
51
+ /// Warns when environment variables are read outside a `config` source
52
+ /// directory or `config.rs`.
53
+ ///
54
+ /// ### Why is this bad?
55
+ ///
56
+ /// Configuration loading should stay centralized so required environment
57
+ /// variables fail with explicit startup errors.
58
+ pub NO_DIRECT_ENV_VAR_OUTSIDE_CONFIG,
59
+ Deny,
60
+ "environment variables should only be read in config modules"
61
+ }
62
+
63
+ declare_lint! {
64
+ /// ### What it does
65
+ ///
66
+ /// Warns when RPC service implementations live outside `rpc/<service>/mod.rs`
67
+ /// or do not have a sibling file for each RPC method.
68
+ ///
69
+ /// ### Why is this bad?
70
+ ///
71
+ /// Each RPC method should have its own file so services stay small and
72
+ /// discoverable.
73
+ pub RPC_METHOD_IN_SEPARATE_FILE,
74
+ Deny,
75
+ "RPC methods should be bridged from rpc/<service>/mod.rs with logic in per-method files"
76
+ }
77
+
78
+ declare_lint! {
79
+ /// ### What it does
80
+ ///
81
+ /// Warns when Axum-style endpoint handlers live outside a `rest` source
82
+ /// directory.
83
+ ///
84
+ /// ### Why is this bad?
85
+ ///
86
+ /// REST and webhook endpoints should be kept away from RPC and database
87
+ /// modules.
88
+ pub REST_ENDPOINT_IN_REST_FOLDER,
89
+ Deny,
90
+ "Axum endpoint handlers should live in rest modules"
91
+ }
92
+
93
+ declare_lint! {
94
+ /// ### What it does
95
+ ///
96
+ /// Warns when `reqwest` is used outside integration modules.
97
+ ///
98
+ /// ### Why is this bad?
99
+ ///
100
+ /// External HTTP clients should be isolated behind integration clients.
101
+ pub NO_HTTP_CLIENT_OUTSIDE_INTEGRATIONS,
102
+ Deny,
103
+ "reqwest usage should live in integrations modules"
104
+ }
105
+
106
+ declare_lint! {
107
+ /// ### What it does
108
+ ///
109
+ /// Warns when Connect RPC error types are used outside RPC modules.
110
+ ///
111
+ /// ### Why is this bad?
112
+ ///
113
+ /// Lower layers should return application errors and let the RPC boundary
114
+ /// map them to transport errors.
115
+ pub RPC_ERRORS_MAPPED_AT_BOUNDARY,
116
+ Deny,
117
+ "Connect RPC errors should be mapped in rpc modules"
118
+ }
119
+
120
+ declare_lint! {
121
+ /// ### What it does
122
+ ///
123
+ /// Warns when Axum response error types are used outside REST modules.
124
+ ///
125
+ /// ### Why is this bad?
126
+ ///
127
+ /// Lower layers should avoid HTTP response concerns.
128
+ pub REST_ERRORS_MAPPED_AT_BOUNDARY,
129
+ Deny,
130
+ "Axum response errors should be mapped in rest modules"
131
+ }
132
+
133
+ declare_lint! {
134
+ /// ### What it does
135
+ ///
136
+ /// Warns when SQL passed to `sqlx::query*` is not a string literal.
137
+ ///
138
+ /// ### Why is this bad?
139
+ ///
140
+ /// Static SQL and sqlx macros are easier to audit and avoid accidental
141
+ /// interpolation bugs.
142
+ pub NO_SQL_STRING_CONSTRUCTION,
143
+ Warn,
144
+ "sqlx query strings should be static"
145
+ }
146
+
147
+ declare_lint! {
148
+ /// ### What it does
149
+ ///
150
+ /// Warns when `src/lib.rs` exports a top-level module outside the approved
151
+ /// architecture areas.
152
+ ///
153
+ /// ### Why is this bad?
154
+ ///
155
+ /// Public module sprawl makes architecture boundaries harder to keep clear.
156
+ pub CONTROLLED_PUBLIC_MODULES,
157
+ Warn,
158
+ "top-level public modules should be approved architecture areas"
159
+ }
160
+
161
+ #[unsafe(no_mangle)]
162
+ pub fn register_lints(sess: &Session, lint_store: &mut LintStore) {
163
+ dylint_linting::init_config(sess);
164
+
165
+ lint_store.register_lints(&[
166
+ MAX_FILE_LENGTH,
167
+ SQLX_ONLY_IN_DB_FOLDER,
168
+ NO_DIRECT_ENV_VAR_OUTSIDE_CONFIG,
169
+ RPC_METHOD_IN_SEPARATE_FILE,
170
+ REST_ENDPOINT_IN_REST_FOLDER,
171
+ NO_HTTP_CLIENT_OUTSIDE_INTEGRATIONS,
172
+ RPC_ERRORS_MAPPED_AT_BOUNDARY,
173
+ REST_ERRORS_MAPPED_AT_BOUNDARY,
174
+ NO_SQL_STRING_CONSTRUCTION,
175
+ CONTROLLED_PUBLIC_MODULES,
176
+ ]);
177
+ lint_store.register_late_pass(|_| Box::<BackboneServerLints>::default());
178
+ }
179
+
180
+ #[derive(Default)]
181
+ struct BackboneServerLints {
182
+ checked_files: HashSet<String>,
183
+ }
184
+
185
+ impl_lint_pass!(BackboneServerLints => [
186
+ MAX_FILE_LENGTH,
187
+ SQLX_ONLY_IN_DB_FOLDER,
188
+ NO_DIRECT_ENV_VAR_OUTSIDE_CONFIG,
189
+ RPC_METHOD_IN_SEPARATE_FILE,
190
+ REST_ENDPOINT_IN_REST_FOLDER,
191
+ NO_HTTP_CLIENT_OUTSIDE_INTEGRATIONS,
192
+ RPC_ERRORS_MAPPED_AT_BOUNDARY,
193
+ REST_ERRORS_MAPPED_AT_BOUNDARY,
194
+ NO_SQL_STRING_CONSTRUCTION,
195
+ CONTROLLED_PUBLIC_MODULES,
196
+ ]);
197
+
198
+ impl<'tcx> LateLintPass<'tcx> for BackboneServerLints {
199
+ fn check_crate(&mut self, cx: &LateContext<'tcx>) {
200
+ let source_map = cx.tcx.sess.source_map();
201
+
202
+ for file in source_map.files().iter() {
203
+ self.check_file_length(cx, &file);
204
+ }
205
+ }
206
+
207
+ fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
208
+ if let ItemKind::Use(path, _) = item.kind {
209
+ let segments = path_segments(path.segments);
210
+
211
+ if is_sqlx_path(&segments) && !is_in_dir(cx, item.span, "db") {
212
+ span_lint(
213
+ cx,
214
+ SQLX_ONLY_IN_DB_FOLDER,
215
+ item.span,
216
+ "sqlx imports are only allowed in db modules",
217
+ );
218
+ }
219
+
220
+ if is_env_path(&segments) && !is_in_config(cx, item.span) {
221
+ span_lint(
222
+ cx,
223
+ NO_DIRECT_ENV_VAR_OUTSIDE_CONFIG,
224
+ item.span,
225
+ "environment variable access should be centralized in config modules",
226
+ );
227
+ }
228
+
229
+ check_boundary_path(cx, item.span, &segments);
230
+ }
231
+
232
+ if let ItemKind::Fn { ident, .. } = item.kind
233
+ && is_rest_handler_name(ident.name.as_str())
234
+ && !is_in_dir(cx, item.span, "rest")
235
+ {
236
+ span_lint(
237
+ cx,
238
+ REST_ENDPOINT_IN_REST_FOLDER,
239
+ item.span,
240
+ "Axum endpoint handlers should live in rest modules",
241
+ );
242
+ }
243
+
244
+ if is_unapproved_public_module(cx, item) {
245
+ span_lint(
246
+ cx,
247
+ CONTROLLED_PUBLIC_MODULES,
248
+ item.span,
249
+ "top-level public modules should be one of config, db, integrations, proto, rest, rpc, or state",
250
+ );
251
+ }
252
+
253
+ if item_contains_dynamic_sql_query(cx, item) {
254
+ span_lint(
255
+ cx,
256
+ NO_SQL_STRING_CONSTRUCTION,
257
+ item.span,
258
+ "sqlx query strings should be string literals or sqlx macros",
259
+ );
260
+ }
261
+
262
+ if let ItemKind::Impl(impl_) = item.kind
263
+ && let Some(of_trait) = impl_.of_trait
264
+ {
265
+ let trait_segments = path_segments(of_trait.trait_ref.path.segments);
266
+ let Some(service_dir) = service_dir_from_trait(&trait_segments) else {
267
+ return;
268
+ };
269
+
270
+ for &item_ref in impl_.items {
271
+ let impl_item = cx.tcx.hir_impl_item(item_ref);
272
+
273
+ if !matches!(impl_item.kind, ImplItemKind::Fn(..)) {
274
+ continue;
275
+ }
276
+
277
+ let method_name = impl_item.ident.name.as_str();
278
+
279
+ if !is_rpc_bridge_module(cx, impl_item.span, &service_dir)
280
+ || !rpc_method_file_exists(cx, impl_item.span, &method_name)
281
+ {
282
+ span_lint(
283
+ cx,
284
+ RPC_METHOD_IN_SEPARATE_FILE,
285
+ impl_item.span,
286
+ "RPC method implementations should be bridged from rpc/<service>/mod.rs with logic in a sibling method file",
287
+ );
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
294
+ if let ExprKind::Path(qpath) = expr.kind {
295
+ let segments = qpath_segments(qpath);
296
+
297
+ if is_sqlx_path(&segments) && !is_in_dir(cx, expr.span, "db") {
298
+ span_lint(
299
+ cx,
300
+ SQLX_ONLY_IN_DB_FOLDER,
301
+ expr.span,
302
+ "sqlx calls are only allowed in db modules",
303
+ );
304
+ }
305
+
306
+ if is_env_path(&segments) && !is_in_config(cx, expr.span) {
307
+ span_lint(
308
+ cx,
309
+ NO_DIRECT_ENV_VAR_OUTSIDE_CONFIG,
310
+ expr.span,
311
+ "environment variable access should be centralized in config modules",
312
+ );
313
+ }
314
+
315
+ check_boundary_path(cx, expr.span, &segments);
316
+ }
317
+
318
+ if let ExprKind::Call(callee, args) = expr.kind {
319
+ let sql = args.first();
320
+
321
+ if is_sqlx_query_callee(cx, callee) && is_dynamic_sql_expr(cx, expr, sql) {
322
+ span_lint(
323
+ cx,
324
+ NO_SQL_STRING_CONSTRUCTION,
325
+ sql.map_or(expr.span, |sql| sql.span),
326
+ "sqlx query strings should be string literals or sqlx macros",
327
+ );
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ impl BackboneServerLints {
334
+ fn check_file_length<'tcx>(
335
+ &mut self,
336
+ cx: &LateContext<'tcx>,
337
+ file: &rustc_span::SourceFile,
338
+ ) {
339
+ let path = file_path(&file.name);
340
+
341
+ if !self.checked_files.insert(path.clone()) || should_skip_file_length(&path) {
342
+ return;
343
+ }
344
+
345
+ let line_count = file.count_lines();
346
+
347
+ if line_count > MAX_FILE_LINES {
348
+ span_lint(
349
+ cx,
350
+ MAX_FILE_LENGTH,
351
+ Span::with_root_ctxt(file.start_pos, file.start_pos),
352
+ format!(
353
+ "Rust source file has {line_count} lines; split files above {MAX_FILE_LINES} lines",
354
+ ),
355
+ );
356
+ }
357
+ }
358
+ }
359
+
360
+ fn qpath_segments(qpath: QPath<'_>) -> Vec<String> {
361
+ match qpath {
362
+ QPath::Resolved(_, path) => path_segments(path.segments),
363
+ QPath::TypeRelative(_, segment) => vec![segment.ident.name.as_str().to_string()],
364
+ }
365
+ }
366
+
367
+ fn span_lint<S, M>(cx: &LateContext<'_>, lint: &'static Lint, span: S, message: M)
368
+ where
369
+ S: Into<MultiSpan>,
370
+ M: Into<DiagMessage>,
371
+ {
372
+ let span = span.into();
373
+
374
+ cx.emit_span_lint(
375
+ lint,
376
+ span.clone(),
377
+ DiagDecorator(|diag| {
378
+ diag.primary_message(message);
379
+ diag.span(span);
380
+ }),
381
+ );
382
+ }
383
+
384
+ fn path_segments(segments: &[rustc_hir::PathSegment<'_>]) -> Vec<String> {
385
+ segments
386
+ .iter()
387
+ .map(|segment| segment.ident.name.as_str().to_string())
388
+ .collect()
389
+ }
390
+
391
+ fn is_sqlx_path(segments: &[String]) -> bool {
392
+ segments.first().is_some_and(|segment| segment == "sqlx")
393
+ }
394
+
395
+ fn is_env_path(segments: &[String]) -> bool {
396
+ matches!(
397
+ segments,
398
+ [first, second, third, ..]
399
+ if first == "std" && second == "env" && matches!(third.as_str(), "var" | "var_os")
400
+ ) || matches!(
401
+ segments,
402
+ [first, second, ..] if first == "env" && matches!(second.as_str(), "var" | "var_os")
403
+ )
404
+ }
405
+
406
+ fn check_boundary_path(cx: &LateContext<'_>, span: Span, segments: &[String]) {
407
+ if is_reqwest_path(segments) && !is_in_dir(cx, span, "integrations") {
408
+ span_lint(
409
+ cx,
410
+ NO_HTTP_CLIENT_OUTSIDE_INTEGRATIONS,
411
+ span,
412
+ "reqwest usage should live behind integrations modules",
413
+ );
414
+ }
415
+
416
+ if is_connect_error_path(segments) && !is_in_dir(cx, span, "rpc") {
417
+ span_lint(
418
+ cx,
419
+ RPC_ERRORS_MAPPED_AT_BOUNDARY,
420
+ span,
421
+ "Connect RPC errors should only be mapped in rpc modules",
422
+ );
423
+ }
424
+
425
+ if is_axum_response_error_path(segments) && !is_in_dir(cx, span, "rest") {
426
+ span_lint(
427
+ cx,
428
+ REST_ERRORS_MAPPED_AT_BOUNDARY,
429
+ span,
430
+ "Axum response error types should only be mapped in rest modules",
431
+ );
432
+ }
433
+ }
434
+
435
+ fn is_reqwest_path(segments: &[String]) -> bool {
436
+ segments.first().is_some_and(|segment| segment == "reqwest")
437
+ }
438
+
439
+ fn is_connect_error_path(segments: &[String]) -> bool {
440
+ segments.first().is_some_and(|segment| segment == "connectrpc")
441
+ && segments.iter().any(|segment| {
442
+ matches!(segment.as_str(), "ConnectError" | "ErrorCode")
443
+ })
444
+ }
445
+
446
+ fn is_axum_response_error_path(segments: &[String]) -> bool {
447
+ segments.first().is_some_and(|segment| segment == "axum")
448
+ && segments.iter().any(|segment| {
449
+ matches!(
450
+ segment.as_str(),
451
+ "StatusCode" | "IntoResponse" | "Response"
452
+ )
453
+ })
454
+ }
455
+
456
+ fn is_sqlx_query_call(segments: &[String]) -> bool {
457
+ matches!(segments, [first, second, ..] if first == "sqlx" && second.starts_with("query"))
458
+ }
459
+
460
+ fn is_sqlx_query_callee(cx: &LateContext<'_>, callee: &Expr<'_>) -> bool {
461
+ if let ExprKind::Path(qpath) = callee.kind
462
+ && is_sqlx_query_call(&qpath_segments(qpath))
463
+ {
464
+ return true;
465
+ }
466
+
467
+ span_snippet(cx, callee.span).is_some_and(|snippet| {
468
+ let snippet = snippet.trim_start();
469
+ snippet.starts_with("sqlx::query")
470
+ })
471
+ }
472
+
473
+ fn is_dynamic_sql_expr(cx: &LateContext<'_>, call: &Expr<'_>, _sql: Option<&Expr<'_>>) -> bool {
474
+ span_snippet(cx, call.span).is_some_and(|snippet| snippet.contains("format!("))
475
+ }
476
+
477
+ fn item_contains_dynamic_sql_query(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
478
+ matches!(item.kind, ItemKind::Fn { .. })
479
+ && span_snippet(cx, item.span).is_some_and(|snippet| {
480
+ snippet.contains("sqlx::query(format!(")
481
+ || snippet.contains("sqlx::query_as(format!(")
482
+ })
483
+ }
484
+
485
+ fn service_dir_from_trait(segments: &[String]) -> Option<String> {
486
+ let trait_name = segments.last()?;
487
+ let service_name = trait_name.strip_suffix("Service")?;
488
+
489
+ if service_name.is_empty() {
490
+ return None;
491
+ }
492
+
493
+ Some(to_snake_case(service_name))
494
+ }
495
+
496
+ fn is_rest_handler_name(name: &str) -> bool {
497
+ name.ends_with("_handler")
498
+ }
499
+
500
+ fn is_unapproved_public_module(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
501
+ let path = span_path(cx, item.span);
502
+
503
+ if !path.ends_with("/src/lib.rs") && !path.ends_with("ui/lib.rs") {
504
+ return false;
505
+ }
506
+
507
+ let ItemKind::Mod(ident, ..) = item.kind else {
508
+ return false;
509
+ };
510
+
511
+ if is_allowed_public_module(ident.name.as_str()) {
512
+ return false;
513
+ }
514
+
515
+ cx.tcx
516
+ .sess
517
+ .source_map()
518
+ .span_to_snippet(item.span)
519
+ .is_ok_and(|snippet| snippet.trim_start().starts_with("pub mod "))
520
+ }
521
+
522
+ fn is_allowed_public_module(name: &str) -> bool {
523
+ matches!(
524
+ name,
525
+ "config" | "db" | "integrations" | "proto" | "rest" | "rpc" | "state"
526
+ )
527
+ }
528
+
529
+ fn is_in_config(cx: &LateContext<'_>, span: Span) -> bool {
530
+ let path = span_path(cx, span);
531
+
532
+ path.ends_with("/config.rs") || has_path_component(&path, "config")
533
+ }
534
+
535
+ fn span_snippet(cx: &LateContext<'_>, span: Span) -> Option<String> {
536
+ cx.tcx.sess.source_map().span_to_snippet(span).ok()
537
+ }
538
+
539
+ fn is_in_dir(cx: &LateContext<'_>, span: Span, dirname: &str) -> bool {
540
+ has_path_component(&span_path(cx, span), dirname)
541
+ }
542
+
543
+ fn is_rpc_bridge_module(cx: &LateContext<'_>, span: Span, service_dir: &str) -> bool {
544
+ let path = span_path(cx, span);
545
+ path.ends_with(&format!("/rpc/{service_dir}/mod.rs"))
546
+ }
547
+
548
+ fn rpc_method_file_exists(cx: &LateContext<'_>, span: Span, method_name: &str) -> bool {
549
+ let path = span_path(cx, span);
550
+ let Some(dir) = Path::new(&path).parent() else {
551
+ return false;
552
+ };
553
+
554
+ dir.join(format!("{method_name}.rs")).exists()
555
+ }
556
+
557
+ fn has_path_component(path: &str, component: &str) -> bool {
558
+ Path::new(path).components().any(|path_component| {
559
+ matches!(path_component, Component::Normal(name) if name == component)
560
+ })
561
+ }
562
+
563
+ fn should_skip_file_length(path: &str) -> bool {
564
+ !path.ends_with(".rs")
565
+ || path.contains("/target/")
566
+ || path.contains("/.cargo/")
567
+ || path.contains("/rustc/")
568
+ || path.contains("/rust/deps/")
569
+ || path.ends_with("/mod.rs")
570
+ || path.ends_with("/build.rs")
571
+ }
572
+
573
+ fn span_path(cx: &LateContext<'_>, span: Span) -> String {
574
+ source_file(cx, span)
575
+ .map(|file| file_path(&file.name))
576
+ .unwrap_or_default()
577
+ }
578
+
579
+ fn source_file(cx: &LateContext<'_>, span: Span) -> Option<std::sync::Arc<rustc_span::SourceFile>> {
580
+ let source_map = cx.tcx.sess.source_map();
581
+ source_map.lookup_source_file(span.lo()).into()
582
+ }
583
+
584
+ fn file_path(filename: &FileName) -> String {
585
+ filename
586
+ .prefer_local_unconditionally()
587
+ .to_string_lossy()
588
+ .replace('\\', "/")
589
+ }
590
+
591
+ fn to_snake_case(value: &str) -> String {
592
+ let mut output = String::new();
593
+
594
+ for (index, character) in value.chars().enumerate() {
595
+ if character.is_uppercase() {
596
+ if index > 0 {
597
+ output.push('_');
598
+ }
599
+
600
+ output.extend(character.to_lowercase());
601
+ } else {
602
+ output.push(character);
603
+ }
604
+ }
605
+
606
+ output
607
+ }
608
+
609
+ #[test]
610
+ fn ui() {
611
+ dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
612
+ }
@@ -0,0 +1,4 @@
1
+ pub mod config {}
2
+ pub mod surprise {}
3
+
4
+ fn main() {}
@@ -0,0 +1,10 @@
1
+ warning: top-level public modules should be one of config, db, integrations, proto, rest, rpc, or state
2
+ --> $DIR/lib.rs:2:1
3
+ |
4
+ LL | pub mod surprise {}
5
+ | ^^^^^^^^^^^^^^^^^^^
6
+ |
7
+ = note: `#[warn(controlled_public_modules)]` on by default
8
+
9
+ warning: 1 warning emitted
10
+