use crate::proto;
use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position, Range, Url};
use solar_interface::{
Symbol,
data_structures::{Never, map::FxHashMap},
};
use solar_sema::{
Gcx,
builtins::Builtin,
hir::{self, CallArgsKind, ExprKind, ItemId, StmtKind, Visit},
ty::{CallableParamSource, Ty, TyKind},
};
use std::ops::ControlFlow;
#[derive(Clone, Debug, Default)]
pub(crate) struct InlayHintIndex {
by_file: FxHashMap<Url, Vec<StoredInlayHint>>,
}
#[derive(Clone, Debug)]
struct StoredInlayHint {
position: Position,
label: Box<str>,
kind: StoredInlayHintKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum StoredInlayHintKind {
Parameter,
CallType,
}
impl InlayHintIndex {
pub(crate) fn build(gcx: Gcx<'_>) -> Self {
let mut collector = InlayHintCollector { gcx, index: Self::default() };
for source_id in gcx.hir.source_ids() {
let _ = collector.visit_nested_source(source_id);
}
collector.index.sort();
collector.index
}
pub(crate) fn extend(&mut self, other: Self) {
for (uri, hints) in other.by_file {
self.by_file.entry(uri).or_default().extend(hints);
}
self.sort();
}
pub(crate) fn hints(&self, uri: &Url, range: Range) -> Vec<InlayHint> {
let Some(hints) = self.by_file.get(uri) else {
return Vec::new();
};
let start = hints.partition_point(|hint| hint.position < range.start);
let include_end = range.start == range.end;
hints[start..]
.iter()
.take_while(|hint| {
hint.position < range.end || (include_end && hint.position == range.end)
})
.map(StoredInlayHint::to_lsp)
.collect()
}
fn push(&mut self, uri: Url, hint: StoredInlayHint) {
self.by_file.entry(uri).or_default().push(hint);
}
fn sort(&mut self) {
for hints in self.by_file.values_mut() {
hints.sort_by(|a, b| hint_sort_key(a).cmp(&hint_sort_key(b)));
}
}
}
impl StoredInlayHint {
fn parameter(position: Position, label: impl Into<Box<str>>) -> Self {
Self { position, label: label.into(), kind: StoredInlayHintKind::Parameter }
}
fn call_type(position: Position, label: impl Into<Box<str>>) -> Self {
Self { position, label: label.into(), kind: StoredInlayHintKind::CallType }
}
fn to_lsp(&self) -> InlayHint {
let (kind, padding_left, padding_right) = self.kind.lsp_fields();
InlayHint {
position: self.position,
label: InlayHintLabel::String(self.label.to_string()),
kind: Some(kind),
text_edits: None,
tooltip: None,
padding_left: Some(padding_left),
padding_right: Some(padding_right),
data: None,
}
}
}
impl StoredInlayHintKind {
fn lsp_fields(self) -> (InlayHintKind, bool, bool) {
match self {
Self::Parameter => (InlayHintKind::PARAMETER, false, true),
Self::CallType => (InlayHintKind::TYPE, true, false),
}
}
}
struct InlayHintCollector<'gcx> {
gcx: Gcx<'gcx>,
index: InlayHintIndex,
}
impl<'gcx> InlayHintCollector<'gcx> {
fn push_parameter_hints(
&mut self,
args: &hir::CallArgs<'gcx>,
param_source: Option<CallableParamSource>,
) {
let (CallArgsKind::Unnamed(exprs), Some(param_source)) = (args.kind, param_source) else {
return;
};
self.push_parameter_hints_for_exprs(exprs, param_source);
}
fn push_parameter_hints_for_exprs(
&mut self,
exprs: impl IntoIterator<Item = &'gcx hir::Expr<'gcx>>,
param_source: CallableParamSource,
) {
let param_names = self.gcx.callable_param_names(param_source);
self.push_parameter_hints_for_exprs_and_names(exprs, param_names);
}
fn push_parameter_hints_for_exprs_and_names(
&mut self,
exprs: impl IntoIterator<Item = &'gcx hir::Expr<'gcx>>,
param_names: impl IntoIterator<Item = Option<Symbol>>,
) {
for (arg, param_name) in exprs.into_iter().zip(param_names) {
let Some(param_name) = param_name else {
continue;
};
if self.argument_name_matches_param(arg, param_name) {
continue;
}
let Some(location) = proto::span_to_location(self.gcx.sess.source_map(), arg.span)
else {
continue;
};
self.index.push(
location.uri,
StoredInlayHint::parameter(location.range.start, format!("{param_name}:")),
);
}
}
fn push_abi_encode_call_parameter_hints(&mut self, args: &hir::CallArgs<'gcx>) {
let CallArgsKind::Unnamed([target, arguments]) = args.kind else {
return;
};
let Some(param_source) = self.call_param_source_from_expr(target) else {
return;
};
let param_names = self.gcx.callable_param_names(param_source);
let arguments = arguments.peel_parens();
match arguments.kind {
ExprKind::Tuple(components) => {
if components.len() != param_names.len()
|| components.iter().any(|component| component.is_none())
{
return;
}
self.push_parameter_hints_for_exprs_and_names(
components.iter().filter_map(|component| *component),
param_names,
);
}
_ if param_names.len() == 1 => {
self.push_parameter_hints_for_exprs_and_names([arguments], param_names);
}
_ => {}
}
}
fn argument_name_matches_param(&self, arg: &'gcx hir::Expr<'gcx>, param_name: Symbol) -> bool {
let arg = arg.peel_parens();
if let ExprKind::Ident([res]) = arg.kind
&& let Some(variable) = res.as_variable()
&& self.gcx.hir.variable(variable).name.is_some_and(|name| name.name == param_name)
{
return true;
}
self.gcx
.sess
.source_map()
.span_to_snippet(arg.span)
.is_ok_and(|snippet| snippet == param_name.as_str())
}
fn push_call_type_hint(&mut self, expr: &'gcx hir::Expr<'gcx>, callee_ty: Option<Ty<'gcx>>) {
if callee_ty.is_some_and(|ty| matches!(ty.kind, TyKind::Type(_))) {
return;
}
let Some(ty) = self.gcx.type_of_expr(expr.id) else {
return;
};
if ty.is_unit() || ty.references_error() {
return;
}
let Some(location) = proto::span_to_location(self.gcx.sess.source_map(), expr.span) else {
return;
};
self.index.push(
location.uri,
StoredInlayHint::call_type(location.range.end, self.call_type_label(ty)),
);
}
fn call_type_label(&self, ty: Ty<'gcx>) -> String {
if let TyKind::Tuple(tys) = ty.kind {
let tys = tys
.iter()
.map(|ty| ty.display(self.gcx).to_string())
.collect::<Vec<_>>()
.join(", ");
return format!(": ({tys})");
}
format!(": {}", ty.display(self.gcx))
}
fn call_param_source(
&self,
callee: &'gcx hir::Expr<'gcx>,
callee_ty: Option<Ty<'gcx>>,
) -> Option<CallableParamSource> {
if let ExprKind::New(hir_ty) = &callee.kind
&& let TyKind::Contract(id) = self.gcx.type_of_hir_ty(hir_ty).kind
&& let Some(ctor) = self.gcx.hir.contract(id).ctor
{
return Some(CallableParamSource::Function { id: ctor, skips_receiver: false });
}
self.call_param_source_from_expr(callee).or_else(|| {
callee_ty
.and_then(|ty| self.gcx.callable_signature_of_ty(ty))
.and_then(|signature| signature.param_source)
})
}
fn call_param_source_from_expr(
&self,
expr: &'gcx hir::Expr<'gcx>,
) -> Option<CallableParamSource> {
let expr = expr.peel_parens();
if let ExprKind::Ident([res]) = expr.kind
&& let Some(variable) = res.as_variable()
&& matches!(self.gcx.hir.variable(variable).ty.kind, hir::TypeKind::Function(_))
{
return Some(CallableParamSource::FunctionType(variable));
}
if let ExprKind::Member(receiver, ident) = expr.kind
&& let Some(receiver_ty) = self.gcx.type_of_expr(receiver.id)
&& let Some(field) = self.struct_field(receiver_ty, ident.name)
&& matches!(self.gcx.hir.variable(field).ty.kind, hir::TypeKind::Function(_))
{
return Some(CallableParamSource::FunctionType(field));
}
self.gcx
.type_of_expr(expr.id)
.and_then(|ty| self.gcx.callable_signature_of_ty(ty))
.and_then(|signature| signature.param_source)
}
fn struct_field(&self, receiver_ty: Ty<'gcx>, name: Symbol) -> Option<hir::VariableId> {
let struct_id = match receiver_ty.kind {
TyKind::Ref(inner, _) => match inner.kind {
TyKind::Struct(id) => id,
_ => return None,
},
TyKind::Struct(id) => id,
_ => return None,
};
self.gcx.hir.strukt(struct_id).fields.iter().copied().find(|&field| {
self.gcx.hir.variable(field).name.is_some_and(|field_name| field_name.name == name)
})
}
fn modifier_param_source(
&self,
modifier: &'gcx hir::Modifier<'gcx>,
) -> Option<CallableParamSource> {
let id = match modifier.id {
ItemId::Contract(id) => self.gcx.hir.contract(id).ctor?,
ItemId::Function(id) => id,
_ => return None,
};
Some(CallableParamSource::Function { id, skips_receiver: false })
}
}
impl<'gcx> Visit<'gcx> for InlayHintCollector<'gcx> {
type BreakValue = Never;
fn hir(&self) -> &'gcx hir::Hir<'gcx> {
&self.gcx.hir
}
fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
if let ExprKind::Call(callee, ref args, _) = expr.kind {
let callee_ty = self.gcx.type_of_expr(callee.id);
if self.gcx.builtin_callee(callee.id) == Some(Builtin::AbiEncodeCall) {
self.push_abi_encode_call_parameter_hints(args);
}
self.push_parameter_hints(args, self.call_param_source(callee, callee_ty));
self.push_call_type_hint(expr, callee_ty);
}
hir::Visit::walk_expr(self, expr)
}
fn visit_stmt(&mut self, stmt: &'gcx hir::Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
if matches!(stmt.kind, StmtKind::AssemblyBlock(_)) {
return ControlFlow::Continue(());
}
hir::Visit::walk_stmt(self, stmt)
}
fn visit_modifier(
&mut self,
modifier: &'gcx hir::Modifier<'gcx>,
) -> ControlFlow<Self::BreakValue> {
self.push_parameter_hints(&modifier.args, self.modifier_param_source(modifier));
hir::Visit::walk_modifier(self, modifier)
}
}
fn hint_sort_key(hint: &StoredInlayHint) -> (Position, StoredInlayHintKind, &str) {
(hint.position, hint.kind, hint.label.as_ref())
}