Skip to main content

forge_lint/sol/info/
inline_assembly.rs

1use super::InlineAssembly;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{Stmt, StmtKind},
8    interface::{BytePos, Span},
9};
10
11declare_forge_lint!(
12    INLINE_ASSEMBLY,
13    Severity::Info,
14    "inline-assembly",
15    "usage of inline assembly; assembly bypasses Solidity safety features and should be reviewed"
16);
17
18const ASSEMBLY_KW_LEN: u32 = 8;
19const NATSPEC_MEMORY_SAFE_MARKER: &str = "@solidity memory-safe-assembly";
20
21impl<'ast> EarlyLintPass<'ast> for InlineAssembly {
22    fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
23        let StmtKind::Assembly(asm) = &stmt.kind else { return };
24
25        let kw_span = assembly_keyword_span(stmt.span);
26
27        let memory_safe = asm.flags.iter().any(|f| f.value.as_str() == "memory-safe")
28            || has_memory_safe_natspec(ctx, stmt.span.lo());
29
30        let msg = if memory_safe {
31            "inline assembly (declared memory-safe); review business logic and side effects"
32        } else {
33            "inline assembly used; review for memory safety and side effects"
34        };
35
36        ctx.emit_with_msg(&INLINE_ASSEMBLY, kw_span, msg);
37    }
38}
39
40/// Narrows a span to the leading `assembly` keyword to keep diagnostics readable.
41fn assembly_keyword_span(span: Span) -> Span {
42    span.with_hi(span.lo() + BytePos(ASSEMBLY_KW_LEN))
43}
44
45/// Returns `true` when the lines immediately preceding `stmt_lo` form a `///` NatSpec block
46/// containing `@solidity memory-safe-assembly`.
47fn has_memory_safe_natspec(ctx: &LintContext, stmt_lo: BytePos) -> bool {
48    let Some(source_file) = ctx.source_file() else { return false };
49    let src = source_file.src.as_str();
50    let start_pos = source_file.start_pos.to_u32();
51    let lo_abs = stmt_lo.to_u32();
52    if lo_abs < start_pos {
53        return false;
54    }
55    let offset = (lo_abs - start_pos) as usize;
56    if offset > src.len() {
57        return false;
58    }
59
60    for line in src[..offset].lines().rev() {
61        let trimmed = line.trim_start();
62        if trimmed.is_empty() {
63            continue;
64        }
65        let Some(rest) = trimmed.strip_prefix("///") else { return false };
66        if rest.trim_start().starts_with(NATSPEC_MEMORY_SAFE_MARKER) {
67            return true;
68        }
69    }
70    false
71}