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.
- package/README.md +33 -0
- package/bin/create-backbone-template.js +5 -0
- package/package.json +30 -0
- package/src/create-backbone-template.js +204 -0
- package/template/.agents/skills/agent-browser/SKILL.md +55 -0
- package/template/.agents/skills/create-plan/SKILL.md +52 -0
- package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
- package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
- package/template/.agents/skills/implement-plan/SKILL.md +26 -0
- package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/review-plan/SKILL.md +38 -0
- package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
- package/template/.env.schema +30 -0
- package/template/.env.test +6 -0
- package/template/.oxlintrc.json +67 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/.vscode/settings.json +23 -0
- package/template/AGENTS.md +55 -0
- package/template/Cargo.lock +2648 -0
- package/template/Cargo.toml +29 -0
- package/template/Justfile +140 -0
- package/template/README.md +72 -0
- package/template/TODO.md +1 -0
- package/template/_gitignore +12 -0
- package/template/buf.gen.yaml +7 -0
- package/template/buf.yaml +10 -0
- package/template/client/.oxfmtrc.json +8 -0
- package/template/client/.oxlintrc.json +57 -0
- package/template/client/README.md +19 -0
- package/template/client/_gitignore +5 -0
- package/template/client/index.html +12 -0
- package/template/client/package.json +47 -0
- package/template/client/packages/design-system/package.json +19 -0
- package/template/client/packages/design-system/src/index.ts +2 -0
- package/template/client/packages/design-system-basic/package.json +18 -0
- package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
- package/template/client/packages/design-system-basic/src/button.tsx +26 -0
- package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
- package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
- package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
- package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
- package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
- package/template/client/packages/design-system-basic/src/form.tsx +9 -0
- package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
- package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
- package/template/client/packages/design-system-basic/src/index.tsx +15 -0
- package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
- package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
- package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
- package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
- package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
- package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
- package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
- package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
- package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
- package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
- package/template/client/packages/design-system-basic/src/styles.css +254 -0
- package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
- package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
- package/template/client/packages/design-system-basic/src/text.tsx +5 -0
- package/template/client/packages/design-system-contract/package.json +15 -0
- package/template/client/packages/design-system-contract/src/button.ts +10 -0
- package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
- package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
- package/template/client/packages/design-system-contract/src/form.ts +9 -0
- package/template/client/packages/design-system-contract/src/heading.ts +9 -0
- package/template/client/packages/design-system-contract/src/index.ts +13 -0
- package/template/client/packages/design-system-contract/src/inline.ts +7 -0
- package/template/client/packages/design-system-contract/src/layout.ts +8 -0
- package/template/client/packages/design-system-contract/src/loader.ts +7 -0
- package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
- package/template/client/packages/design-system-contract/src/notice.ts +8 -0
- package/template/client/packages/design-system-contract/src/stack.ts +8 -0
- package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
- package/template/client/packages/design-system-contract/src/text.ts +9 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
- package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
- package/template/client/packages/design-system-lint/package.json +23 -0
- package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
- package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
- package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
- package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
- package/template/client/packages/design-system-lint/src/rules.ts +111 -0
- package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
- package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
- package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
- package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
- package/template/client/packages/design-system-lint/tsconfig.json +29 -0
- package/template/client/src/App.tsx +77 -0
- package/template/client/src/design-system-components.test.tsx +75 -0
- package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
- package/template/client/src/main.tsx +18 -0
- package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
- package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
- package/template/client/src/pages/hello/hello-page.tsx +126 -0
- package/template/client/src/pages/page.ts +20 -0
- package/template/client/src/testing/create-preview-events.test.ts +36 -0
- package/template/client/src/testing/create-preview-events.ts +30 -0
- package/template/client/src/vite-env.d.ts +1 -0
- package/template/client/tsconfig.json +32 -0
- package/template/client/vite.config.ts +21 -0
- package/template/client/vite.ladle.config.ts +5 -0
- package/template/e2e/.gherkin-lintrc +20 -0
- package/template/e2e/.oxfmtrc.json +15 -0
- package/template/e2e/.oxlintrc.json +37 -0
- package/template/e2e/_gitignore +4 -0
- package/template/e2e/features/helloworld.feature +10 -0
- package/template/e2e/package.json +42 -0
- package/template/e2e/playwright.config.ts +16 -0
- package/template/e2e/support/app-gherkin.ts +4 -0
- package/template/e2e/support/fixtures.ts +236 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
- package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
- package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
- package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
- package/template/e2e/support/gherkin.test.ts +184 -0
- package/template/e2e/support/gherkin.ts +321 -0
- package/template/e2e/support/oxlint-plugin.test.ts +328 -0
- package/template/e2e/support/oxlint-plugin.ts +485 -0
- package/template/e2e/tests/helloworld.spec.ts +39 -0
- package/template/e2e/tsconfig.json +26 -0
- package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
- package/template/package.json +9 -0
- package/template/pnpm-lock.yaml +10723 -0
- package/template/pnpm-workspace.yaml +8 -0
- package/template/pr-slide/README.md +95 -0
- package/template/pr-slide/package.json +23 -0
- package/template/pr-slide/src/cli.js +262 -0
- package/template/pr-slide/src/generate-pr-deck.js +833 -0
- package/template/pr-slide/src/git-context.js +91 -0
- package/template/pr-slide/src/presentation-paths.js +9 -0
- package/template/pr-slide/src/presentations.js +53 -0
- package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
- package/template/pr-slide/test/presentation-paths.test.js +14 -0
- package/template/pr-slide/test/presentations.test.js +50 -0
- package/template/proto/helloworld/v1/helloworld.proto +15 -0
- package/template/scripts/run-e2e.sh +10 -0
- package/template/server/Cargo.toml +26 -0
- package/template/server/build.rs +9 -0
- package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
- package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
- package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
- package/template/server/dylint/backbone_server_lints/README.md +5 -0
- package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
- package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
- package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
- package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
- package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
- package/template/server/migrations/20260520120000_create_projects.sql +12 -0
- package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
- package/template/server/src/config.rs +27 -0
- package/template/server/src/db/hello_world.rs +34 -0
- package/template/server/src/db/hello_world_tests.rs +11 -0
- package/template/server/src/db/mod.rs +39 -0
- package/template/server/src/lib.rs +10 -0
- package/template/server/src/main.rs +43 -0
- package/template/server/src/rpc/greeter/mod.rs +31 -0
- package/template/server/src/rpc/greeter/say_hello.rs +27 -0
- package/template/server/src/rpc/mod.rs +8 -0
- package/template/server/src/state.rs +13 -0
- 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 @@
|
|
|
1
|
+
/target
|
|
@@ -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,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
|
+
|