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,
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        // Keep the diagnostic highlight on the leading `assembly` keyword.
26        let kw_span = stmt.span.with_hi(stmt.span.lo() + BytePos(ASSEMBLY_KW_LEN));
27
28        let memory_safe = asm.flags.iter().any(|f| f.value.as_str() == "memory-safe")
29            || has_memory_safe_natspec(ctx, stmt.span.lo());
30
31        let msg = if memory_safe {
32            "inline assembly (declared memory-safe); review business logic and side effects"
33        } else {
34            "inline assembly used; review for memory safety and side effects"
35        };
36
37        ctx.emit_with_msg(&INLINE_ASSEMBLY, kw_span, msg);
38    }
39}
40
41/// Returns `true` when the lines immediately preceding `stmt_lo` form a `///` NatSpec block
42/// containing `@solidity memory-safe-assembly`.
43fn has_memory_safe_natspec(ctx: &LintContext, stmt_lo: BytePos) -> bool {
44    let Some(source_file) = ctx.source_file() else { return false };
45    let src = source_file.src.as_str();
46    let start_pos = source_file.start_pos.to_u32();
47    let lo_abs = stmt_lo.to_u32();
48    if lo_abs < start_pos {
49        return false;
50    }
51    let offset = (lo_abs - start_pos) as usize;
52    if offset > src.len() {
53        return false;
54    }
55
56    for line in src[..offset].lines().rev() {
57        let trimmed = line.trim_start();
58        if trimmed.is_empty() {
59            continue;
60        }
61        let Some(rest) = trimmed.strip_prefix("///") else { return false };
62        if rest.trim_start().starts_with(NATSPEC_MEMORY_SAFE_MARKER) {
63            return true;
64        }
65    }
66    false
67}