Skip to main content

forge_lint/sol/info/
redundant_base_constructor_call.rs

1use super::RedundantBaseConstructorCall;
2use crate::{
3    linter::{LateLintPass, LintContext, Suggestion},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    interface::{BytePos, Span, diagnostics::Applicability},
8    sema::hir::{self, ItemId},
9};
10
11declare_forge_lint!(
12    REDUNDANT_BASE_CONSTRUCTOR_CALL,
13    Severity::Info,
14    "redundant-base-constructor-call",
15    "explicit empty base-constructor arguments are redundant"
16);
17
18impl<'hir> LateLintPass<'hir> for RedundantBaseConstructorCall {
19    fn check_contract(
20        &mut self,
21        ctx: &LintContext,
22        _gcx: solar::sema::Gcx<'hir>,
23        hir: &'hir hir::Hir<'hir>,
24        contract: &'hir hir::Contract<'hir>,
25    ) {
26        // `contract X is A(...), B(...)` clauses.
27        // Removing only the `()` is enough: `is A` is valid Solidity.
28        for m in contract.bases_args {
29            try_emit(ctx, hir, m, m.args.span);
30        }
31    }
32
33    fn check_function(
34        &mut self,
35        ctx: &LintContext,
36        _gcx: solar::sema::Gcx<'hir>,
37        hir: &'hir hir::Hir<'hir>,
38        func: &'hir hir::Function<'hir>,
39    ) {
40        // `constructor() A(...) {}` modifier-style base calls.
41        if !matches!(func.kind, hir::FunctionKind::Constructor) {
42            return;
43        }
44        for m in func.modifiers {
45            // Base-constructor invocations resolve to a contract; real modifiers resolve to
46            // functions.
47            if matches!(m.id, ItemId::Contract(_)) {
48                // The bare base name `A` (without parens) is not valid in a constructor's
49                // modifier list, so the whole `A()` must be removed. Extend the span to also
50                // swallow one leading whitespace char to avoid leaving a double space.
51                try_emit(ctx, hir, m, expand_to_leading_ws(ctx, m.span));
52            }
53        }
54    }
55}
56
57fn try_emit<'hir>(
58    ctx: &LintContext,
59    hir: &'hir hir::Hir<'hir>,
60    m: &'hir hir::Modifier<'hir>,
61    fix_span: Span,
62) {
63    let ItemId::Contract(base_id) = m.id else { return };
64
65    // `is A` (no parens written) — nothing to flag.
66    if m.args.is_dummy() {
67        return;
68    }
69    // `A(args...)` with real arguments — not redundant.
70    if !m.args.is_empty() {
71        return;
72    }
73
74    // Empty `()`. Redundant only when the base ctor takes no parameters
75    // (or the base declares no constructor at all).
76    let base = hir.contract(base_id);
77    let redundant = match base.ctor {
78        None => true,
79        Some(c) => hir.function(c).parameters.is_empty(),
80    };
81    if !redundant {
82        return;
83    }
84
85    // Only emit a machine-applicable fix if the args span really is just `()` (no comments,
86    // whitespace, etc. that we'd silently drop). Otherwise fall back to a plain diagnostic.
87    let safe_to_fix = ctx.span_to_snippet(m.args.span).map(|s| s.trim() == "()").unwrap_or(false);
88
89    if safe_to_fix {
90        ctx.emit_with_suggestion(
91            &REDUNDANT_BASE_CONSTRUCTOR_CALL,
92            m.args.span,
93            Suggestion::fix(String::new(), Applicability::MachineApplicable)
94                .with_span(fix_span)
95                .with_desc("remove redundant base-constructor call"),
96        );
97    } else {
98        ctx.emit(&REDUNDANT_BASE_CONSTRUCTOR_CALL, m.args.span);
99    }
100}
101
102/// Extends `span` to start one byte earlier when that byte is an ASCII space or tab.
103///
104/// Used so that removing a modifier-list base call like `A()` from
105/// `constructor() ... A() {}` doesn't leave a stray double space in the source.
106fn expand_to_leading_ws(ctx: &LintContext, span: Span) -> Span {
107    if span.is_dummy() || span.lo() == BytePos(0) {
108        return span;
109    }
110    let prev = Span::new(span.lo() - BytePos(1), span.lo());
111    match ctx.span_to_snippet(prev).as_deref() {
112        Some(" " | "\t") => span.with_lo(span.lo() - BytePos(1)),
113        _ => span,
114    }
115}