Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:26:28.136Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

lib.rs

566 lines · 20.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.