Skip to repository content566 lines · 20.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:26:28.136Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
lib.rs
1#![feature(rustc_private)]
2#![warn(unused_extern_crates)]
3
4extern crate rustc_ast;
5extern crate rustc_errors;
6extern crate rustc_hir;
7extern crate rustc_lint;
8extern crate rustc_middle;
9extern crate rustc_session;
10extern crate rustc_span;
11
12use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then};
13use clippy_utils::is_def_id_trait_method;
14use clippy_utils::source::snippet_opt;
15use rustc_ast::ast::LitKind;
16use rustc_errors::Applicability;
17use rustc_hir::def::{DefKind, Res};
18use rustc_hir::def_id::DefId;
19use rustc_hir::intravisit::{Visitor, walk_expr};
20use rustc_hir::{
21 Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, CoroutineSource, Expr, ExprKind,
22 YieldSource,
23};
24use rustc_lint::{LateContext, LateLintPass};
25use rustc_middle::hir::nested_filter;
26use rustc_middle::ty::Ty;
27use rustc_span::Span;
28
29mod blocking_io_on_foreground;
30mod entity_update_in_render;
31mod notify_in_render;
32mod owned_string_into_shared;
33mod render_helpers;
34
35use blocking_io_on_foreground::BLOCKING_IO_ON_FOREGROUND;
36use entity_update_in_render::ENTITY_UPDATE_IN_RENDER;
37use notify_in_render::NOTIFY_IN_RENDER;
38use owned_string_into_shared::OWNED_STRING_INTO_SHARED;
39
40// ---------------------------------------------------------------------------
41// Boilerplate: export the dylint ABI version symbol.
42// ---------------------------------------------------------------------------
43dylint_linting::dylint_library!();
44
45// ---------------------------------------------------------------------------
46// Registration: a single entry point that hands both lints to the compiler.
47// ---------------------------------------------------------------------------
48#[allow(clippy::no_mangle_with_rust_abi)]
49#[unsafe(no_mangle)]
50pub fn register_lints(sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
51 dylint_linting::init_config(sess);
52 lint_store.register_lints(&[
53 SHARED_STRING_FROM_STR_LITERAL,
54 ASYNC_BLOCK_WITHOUT_AWAIT,
55 BLOCKING_IO_ON_FOREGROUND,
56 ENTITY_UPDATE_IN_RENDER,
57 NOTIFY_IN_RENDER,
58 OWNED_STRING_INTO_SHARED,
59 ]);
60 lint_store.register_late_pass(|_| Box::new(SharedStringFromStrLiteral));
61 lint_store.register_late_pass(|_| Box::new(AsyncBlockWithoutAwait));
62 lint_store.register_late_pass(|_| Box::new(blocking_io_on_foreground::BlockingIoOnForeground));
63 lint_store.register_late_pass(|_| Box::new(entity_update_in_render::EntityUpdateInRender));
64 lint_store.register_late_pass(|_| Box::new(notify_in_render::NotifyInRender));
65 lint_store.register_late_pass(|_| Box::new(owned_string_into_shared::OwnedStringIntoShared));
66}
67
68// ===========================================================================
69// Lint A — SHARED_STRING_FROM_STR_LITERAL
70// ===========================================================================
71
72rustc_session::declare_lint! {
73 /// ### What it does
74 ///
75 /// Flags `gpui::SharedString` values constructed from a string literal by
76 /// any path other than `SharedString::new_static`.
77 ///
78 /// ### Why is this bad?
79 ///
80 /// `SharedString` wraps a `SmolStr`. `SmolStr::from` either copies the
81 /// bytes into inline storage (literals ≤ 23 bytes) or allocates a fresh
82 /// `Arc<str>` on the heap (literals > 23 bytes). `SharedString::new_static`
83 /// does neither: it stores the `'static` pointer directly. For a string
84 /// literal the constant-pointer path is always available and strictly
85 /// cheaper.
86 ///
87 /// This lint fires on `SharedString::from("…")`, `SharedString::new("…")`,
88 /// `<SharedString as From<_>>::from("…")`, and `"…".into()` whose inferred
89 /// target type is `SharedString`. It does not fire on
90 /// `SharedString::new_static(…)`.
91 ///
92 /// The lint distinguishes two tiers of wastefulness:
93 /// * Literals > 23 bytes trigger a heap allocation per call site, flagged
94 /// at full severity.
95 /// * Literals ≤ 23 bytes "only" pay a memcpy; still strictly worse than
96 /// `new_static`, but cheaper to leave alone.
97 ///
98 /// ### Example
99 ///
100 /// ```ignore
101 /// let s: SharedString = SharedString::from("Right-click for more options");
102 /// let t: SharedString = "hello".into();
103 /// ```
104 ///
105 /// Use instead:
106 ///
107 /// ```ignore
108 /// let s = SharedString::new_static("Right-click for more options");
109 /// let t = SharedString::new_static("hello");
110 /// ```
111 pub SHARED_STRING_FROM_STR_LITERAL,
112 Warn,
113 "constructing a `SharedString` from a string literal via a copying/allocating path"
114}
115
116rustc_session::declare_lint_pass!(SharedStringFromStrLiteral => [SHARED_STRING_FROM_STR_LITERAL]);
117
118/// Maximum number of bytes that `SmolStr` (and therefore `SharedString`) can
119/// store inline on 64-bit targets. Literals larger than this trigger an
120/// `Arc<str>` allocation on every conversion.
121///
122/// Source: `smol_str` v0.3's `INLINE_CAP`. See
123/// <https://docs.rs/smol_str/0.3.6/smol_str/>.
124const SMOL_STR_INLINE_CAP: usize = 23;
125
126impl<'tcx> LateLintPass<'tcx> for SharedStringFromStrLiteral {
127 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
128 // Do not descend into macro-expanded code: we'd be suggesting edits to
129 // spans the user cannot actually touch.
130 if expr.span.from_expansion() {
131 return;
132 }
133
134 let ty = cx.typeck_results().expr_ty(expr);
135 if !is_shared_string(cx, ty) {
136 return;
137 }
138
139 let Some(literal) = extract_literal_source(cx, expr) else {
140 return;
141 };
142
143 emit_shared_string(cx, expr.span, literal);
144 }
145}
146
147/// A string literal the user wrote and that we are confident we can replace.
148struct LiteralSource {
149 /// The literal's decoded contents.
150 contents: String,
151 /// The span of the full expression that should be replaced (e.g. the whole
152 /// `SharedString::from("x")` call), not just the literal token.
153 replace_span: Span,
154}
155
156fn extract_literal_source<'tcx>(
157 cx: &LateContext<'tcx>,
158 expr: &'tcx Expr<'tcx>,
159) -> Option<LiteralSource> {
160 match &expr.kind {
161 // `SharedString::from(lit)`, `SharedString::new(lit)`, or any other
162 // associated/trait function resolving onto `SharedString` with a
163 // single string-literal argument.
164 ExprKind::Call(func, [arg]) => {
165 let def_id = call_def_id(cx, func)?;
166 if !is_interesting_shared_string_constructor(cx, def_id) {
167 return None;
168 }
169 let contents = str_literal_contents(arg)?;
170 Some(LiteralSource {
171 contents,
172 replace_span: expr.span,
173 })
174 }
175
176 // `lit.into()` where the target type is `SharedString`.
177 ExprKind::MethodCall(path_seg, receiver, [], _)
178 if path_seg.ident.name.as_str() == "into" =>
179 {
180 let contents = str_literal_contents(receiver)?;
181 Some(LiteralSource {
182 contents,
183 replace_span: expr.span,
184 })
185 }
186
187 _ => None,
188 }
189}
190
191/// Extract the contents of a string literal expression, peeling through a
192/// single layer of reference if the user wrote `&"lit"`.
193fn str_literal_contents<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<String> {
194 let inner = match &expr.kind {
195 ExprKind::AddrOf(_, _, inner) => *inner,
196 _ => expr,
197 };
198 if let ExprKind::Lit(lit) = &inner.kind
199 && let LitKind::Str(sym, _) = lit.node
200 {
201 Some(sym.as_str().to_owned())
202 } else {
203 None
204 }
205}
206
207/// Returns the `DefId` of the function being called, if `func` is a direct
208/// path to a function or associated function. This handles both
209/// `Type::method(...)` syntax (including type-relative paths that resolve
210/// through `typeck_results`) and free-function paths.
211fn call_def_id<'tcx>(cx: &LateContext<'tcx>, func: &'tcx Expr<'tcx>) -> Option<DefId> {
212 let ExprKind::Path(qpath) = &func.kind else {
213 return None;
214 };
215 match cx.qpath_res(qpath, func.hir_id) {
216 Res::Def(DefKind::Fn | DefKind::AssocFn, def_id) => Some(def_id),
217 _ => None,
218 }
219}
220
221/// True if `def_id` names a `SharedString` constructor that we treat as a
222/// wasteful alternative to `SharedString::new_static` when passed a string
223/// literal.
224///
225/// This covers two distinct resolutions rustc produces for these call sites:
226///
227/// * `SharedString::new("x")` resolves to the inherent associated function
228/// on `impl SharedString`. The impl's `Self` type is `SharedString`.
229/// * `SharedString::from("x")` resolves to the trait method
230/// `core::convert::From::from`. The impl is not recorded on the `def_id`
231/// itself; we instead verify the enclosing trait is `From` and rely on the
232/// caller having already checked that the call's result type is
233/// `SharedString`.
234///
235/// `SharedString::new_static` is explicitly exempted because it is the
236/// preferred alternative.
237fn is_interesting_shared_string_constructor(cx: &LateContext<'_>, def_id: DefId) -> bool {
238 let tcx = cx.tcx;
239 let name = tcx.item_name(def_id);
240 if name.as_str() == "new_static" {
241 return false;
242 }
243 if !matches!(name.as_str(), "from" | "new") {
244 return false;
245 }
246 if let Some(impl_id) = tcx.impl_of_assoc(def_id) {
247 let self_ty = tcx.type_of(impl_id).skip_binder();
248 return is_shared_string(cx, self_ty);
249 }
250 if let Some(trait_id) = tcx.trait_of_assoc(def_id) {
251 // The caller has already asserted that the call's result type is
252 // `SharedString`, so a `From::from` call resolving here is
253 // equivalent to `<SharedString as From<_>>::from`.
254 let path = tcx.def_path_str(trait_id);
255 return path == "core::convert::From" || path == "std::convert::From";
256 }
257 false
258}
259
260/// Match the canonical definition path of `gpui_shared_string::SharedString`.
261/// Re-exports through `gpui` resolve back to the same `DefId`.
262fn is_shared_string(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
263 let Some(adt) = ty.ty_adt_def() else {
264 return false;
265 };
266 let did = adt.did();
267 let krate = cx.tcx.crate_name(did.krate);
268 if krate.as_str() != "gpui_shared_string" {
269 return false;
270 }
271 cx.tcx.item_name(did).as_str() == "SharedString"
272}
273
274fn emit_shared_string(cx: &LateContext<'_>, call_span: Span, literal: LiteralSource) {
275 let LiteralSource {
276 contents,
277 replace_span,
278 } = literal;
279 let byte_len = contents.len();
280 let over_inline = byte_len > SMOL_STR_INLINE_CAP;
281
282 // Use the original source text for the replacement where possible so we
283 // preserve raw-string syntax, escapes, etc. Fall back to debug-formatting
284 // the decoded contents if the source is unavailable (e.g. macro-generated).
285 let replacement_lit = snippet_opt(cx, replace_span)
286 .and_then(extract_embedded_string_literal)
287 .unwrap_or_else(|| format!("{contents:?}"));
288
289 let suggestion = format!("SharedString::new_static({replacement_lit})");
290
291 let primary_msg = if over_inline {
292 "this `SharedString` construction heap-allocates on every call"
293 } else {
294 "this `SharedString` construction copies the literal on every call"
295 };
296
297 span_lint_and_then(
298 cx,
299 SHARED_STRING_FROM_STR_LITERAL,
300 call_span,
301 primary_msg,
302 |diag| {
303 if over_inline {
304 diag.note(format!(
305 "the literal is {byte_len} bytes, which exceeds `SmolStr`'s {SMOL_STR_INLINE_CAP}-byte inline capacity, so `SmolStr::from` allocates an `Arc<str>` here",
306 ));
307 } else {
308 diag.note(format!(
309 "the literal is {byte_len} bytes (≤ {SMOL_STR_INLINE_CAP}) so it stays inline, but the copy is still avoidable",
310 ));
311 }
312 diag.note("`SharedString::new_static` stores the `'static` pointer directly and performs no allocation or copy");
313 diag.span_suggestion(
314 replace_span,
315 "use the zero-cost static constructor",
316 suggestion,
317 Applicability::MachineApplicable,
318 );
319 },
320 );
321}
322
323/// Given a snippet like `SharedString::from("hi")` or `"hi".into()`, extract
324/// the first embedded string literal token (including any `r#"..."#` prefix)
325/// so we can paste it back unchanged. This is a best-effort scanner and
326/// returns `None` when the snippet has no literal or an unterminated one.
327fn extract_embedded_string_literal(snippet: String) -> Option<String> {
328 let bytes = snippet.as_bytes();
329 let mut i = 0;
330 while i < bytes.len() {
331 // Raw string: optional `b`, then `r`, then `#`*, then `"`.
332 let raw_start = i;
333 let mut j = i;
334 if j < bytes.len() && bytes[j] == b'b' {
335 j += 1;
336 }
337 if j < bytes.len() && bytes[j] == b'r' {
338 let mut hashes = 0;
339 let mut k = j + 1;
340 while k < bytes.len() && bytes[k] == b'#' {
341 hashes += 1;
342 k += 1;
343 }
344 if k < bytes.len() && bytes[k] == b'"' {
345 // Scan for closing `"` followed by the same number of `#`.
346 let mut m = k + 1;
347 while m < bytes.len() {
348 if bytes[m] == b'"' {
349 let mut close_hashes = 0;
350 let mut n = m + 1;
351 while close_hashes < hashes && n < bytes.len() && bytes[n] == b'#' {
352 close_hashes += 1;
353 n += 1;
354 }
355 if close_hashes == hashes {
356 return Some(snippet[raw_start..n].to_owned());
357 }
358 }
359 m += 1;
360 }
361 return None;
362 }
363 }
364 // Regular string: optional `b`, then `"`, until matching unescaped `"`.
365 let mut j = i;
366 if j < bytes.len() && bytes[j] == b'b' {
367 j += 1;
368 }
369 if j < bytes.len() && bytes[j] == b'"' {
370 let mut m = j + 1;
371 while m < bytes.len() {
372 match bytes[m] {
373 b'\\' => {
374 m += 2;
375 continue;
376 }
377 b'"' => return Some(snippet[raw_start..=m].to_owned()),
378 _ => m += 1,
379 }
380 }
381 return None;
382 }
383 i += 1;
384 }
385 None
386}
387
388// ===========================================================================
389// Lint B — ASYNC_BLOCK_WITHOUT_AWAIT
390// ===========================================================================
391
392rustc_session::declare_lint! {
393 /// ### What it does
394 ///
395 /// Flags `async { … }` and `async move { … }` blocks whose body contains
396 /// no `.await` expression at their own nesting level.
397 ///
398 /// ### Why is this bad?
399 ///
400 /// An async block without an `.await` wraps synchronous code in a `Future`
401 /// state machine for no benefit. The state machine adds binary size, and
402 /// the indirection may hide the fact that the code never actually yields.
403 /// Either the `async` should be removed (the code is synchronous), or a
404 /// missing `.await` is a bug.
405 ///
406 /// ### Example
407 ///
408 /// ```ignore
409 /// let future = async { compute_something() };
410 /// ```
411 ///
412 /// Use instead:
413 ///
414 /// ```ignore
415 /// let value = compute_something();
416 /// ```
417 pub ASYNC_BLOCK_WITHOUT_AWAIT,
418 Warn,
419 "`async` block that contains no `.await` expression"
420}
421
422rustc_session::declare_lint_pass!(AsyncBlockWithoutAwait => [ASYNC_BLOCK_WITHOUT_AWAIT]);
423
424impl<'tcx> LateLintPass<'tcx> for AsyncBlockWithoutAwait {
425 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
426 if expr.span.from_expansion() {
427 return;
428 }
429
430 // Match only async blocks — not async function bodies or async closures.
431 let ExprKind::Closure(Closure {
432 kind:
433 ClosureKind::Coroutine(CoroutineKind::Desugared(
434 CoroutineDesugaring::Async,
435 CoroutineSource::Block,
436 )),
437 body,
438 ..
439 }) = &expr.kind
440 else {
441 return;
442 };
443
444 // Trait impls are constrained by the trait's signature. If the trait
445 // requires a method that returns a future, the implementor must produce
446 // an async block even when their implementation has nothing to await.
447 let enclosing_body_owner = cx.tcx.hir_enclosing_body_owner(expr.hir_id);
448 if is_def_id_trait_method(cx, enclosing_body_owner) {
449 return;
450 }
451
452 let body = cx.tcx.hir_body(*body);
453 let mut visitor = AwaitVisitor {
454 cx,
455 found_await: false,
456 async_depth: 0,
457 };
458 walk_expr(&mut visitor, body.value);
459
460 if !visitor.found_await {
461 span_lint_and_help(
462 cx,
463 ASYNC_BLOCK_WITHOUT_AWAIT,
464 expr.span,
465 "this `async` block contains no `.await`",
466 None,
467 "consider removing the `async` block or adding the missing `.await`",
468 );
469 }
470 }
471}
472
473/// Walks the body of an async block looking for `.await` expressions. Tracks
474/// nesting depth so that an `.await` inside a *nested* async block is not
475/// attributed to the *outer* block.
476struct AwaitVisitor<'a, 'tcx> {
477 cx: &'a LateContext<'tcx>,
478 found_await: bool,
479 async_depth: usize,
480}
481
482impl<'tcx> Visitor<'tcx> for AwaitVisitor<'_, 'tcx> {
483 type NestedFilter = nested_filter::OnlyBodies;
484
485 fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
486 self.cx.tcx
487 }
488
489 fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
490 if let ExprKind::Yield(_, YieldSource::Await { .. }) = expr.kind {
491 if self.async_depth == 0 {
492 self.found_await = true;
493 return;
494 }
495 }
496
497 let is_nested_async_block = matches!(
498 expr.kind,
499 ExprKind::Closure(Closure {
500 kind: ClosureKind::Coroutine(CoroutineKind::Desugared(
501 CoroutineDesugaring::Async,
502 _
503 )),
504 ..
505 })
506 );
507
508 if is_nested_async_block {
509 self.async_depth += 1;
510 }
511
512 walk_expr(self, expr);
513
514 if is_nested_async_block {
515 self.async_depth -= 1;
516 }
517 }
518}
519
520// ===========================================================================
521// Tests
522// ===========================================================================
523
524#[cfg(test)]
525mod tests {
526 use std::path::PathBuf;
527 use std::process::Command;
528
529 /// Build the test-fixture `gpui` crate and return rustc flags that make
530 /// it available to standalone UI test files via `extern crate gpui`.
531 fn gpui_fixture_rustc_flags() -> Vec<String> {
532 let fixture_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "test_fixture"]
533 .iter()
534 .collect();
535
536 let status = Command::new("cargo")
537 .args(["build", "--package", "gpui"])
538 .current_dir(&fixture_dir)
539 .status()
540 .expect("failed to run cargo build for gpui fixture");
541 assert!(status.success(), "gpui fixture build failed");
542
543 let rlib: PathBuf = fixture_dir.join("target/debug/libgpui.rlib");
544 let deps: PathBuf = fixture_dir.join("target/debug/deps");
545
546 vec![
547 "--edition=2021".to_string(),
548 format!("--extern=gpui={}", rlib.display()),
549 format!("-Ldependency={}", deps.display()),
550 ]
551 }
552
553 #[test]
554 fn ui() {
555 let flags = gpui_fixture_rustc_flags();
556 dylint_testing::ui::Test::src_base(env!("CARGO_PKG_NAME"), "ui")
557 .rustc_flags(flags)
558 .run();
559 }
560
561 #[test]
562 fn ui_shared_string() {
563 dylint_testing::ui_test_examples(env!("CARGO_PKG_NAME"));
564 }
565}
566