use solang_forge_fmt::{format_to, parse, FormatterConfig};
use itertools::Itertools;
use num_traits::ToPrimitive;
use rust_lapper::{Interval, Lapper};
use serde_json::Value;
use solang::{
codegen::{self, codegen, Expression},
file_resolver::FileResolver,
parse_and_resolve,
sema::{
ast::{self, RetrieveType, StructType, Type},
builtin::{get_prototype, BUILTIN_FUNCTIONS, BUILTIN_METHODS, BUILTIN_VARIABLE},
builtin_structs::BUILTIN_STRUCTS,
symtable,
tags::render,
},
Target,
};
use solang_parser::pt;
use std::{
collections::{HashMap, HashSet},
ffi::OsString,
path::PathBuf,
};
use tokio::sync::Mutex;
use tower_lsp::{
jsonrpc::{Error, ErrorCode, Result},
lsp_types::{
request::{
GotoDeclarationParams, GotoDeclarationResponse, GotoImplementationParams,
GotoImplementationResponse, GotoTypeDefinitionParams, GotoTypeDefinitionResponse,
},
CompletionContext, CompletionItem, CompletionOptions, CompletionParams, CompletionResponse,
CompletionTriggerKind, DeclarationCapability, Diagnostic, DiagnosticRelatedInformation,
DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams,
ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse,
Hover, HoverContents, HoverParams, HoverProviderCapability,
ImplementationProviderCapability, InitializeParams, InitializeResult, InitializedParams,
Location, MarkedString, MessageType, OneOf, Position, Range, ReferenceParams, RenameParams,
ServerCapabilities, SignatureHelpOptions, TextDocumentContentChangeEvent,
TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit,
TypeDefinitionProviderCapability, Url, WorkspaceEdit, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
},
Client, LanguageServer, LspService, Server,
};
use crate::cli::{target_arg, LanguageServerCommand};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DefinitionType {
Function(usize),
Variable(usize),
NonLocalVariable(Option<usize>, usize),
Struct(StructType),
Field(Type, usize),
Enum(usize),
Variant(usize, usize),
Contract(usize),
Event(usize),
UserType(usize),
DynamicBytes,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DefinitionIndex {
def_path: PathBuf,
def_type: DefinitionType,
}
impl From<DefinitionType> for DefinitionIndex {
fn from(value: DefinitionType) -> Self {
Self {
def_path: Default::default(),
def_type: value,
}
}
}
type Definitions = HashMap<DefinitionIndex, Range>;
type HoverEntry = Interval<usize, String>;
type ReferenceEntry = Interval<usize, DefinitionIndex>;
type ScopeEntry = Interval<usize, Vec<(String, Option<DefinitionIndex>)>>;
type Implementations = HashMap<DefinitionIndex, Vec<DefinitionIndex>>;
type Types = HashMap<DefinitionIndex, DefinitionIndex>;
type Declarations = HashMap<DefinitionIndex, Vec<DefinitionIndex>>;
type Properties = HashMap<DefinitionIndex, HashMap<String, Option<DefinitionIndex>>>;
#[derive(Default)]
struct Files {
caches: HashMap<PathBuf, FileCache>,
text_buffers: HashMap<PathBuf, String>,
}
#[derive(Debug)]
struct FileCache {
file: ast::File,
hovers: Lapper<usize, String>,
references: Lapper<usize, DefinitionIndex>,
scopes: Lapper<usize, Vec<(String, Option<DefinitionIndex>)>>,
top_level_code_objects: HashMap<String, Option<DefinitionIndex>>,
}
#[derive(Default)]
struct GlobalCache {
definitions: Definitions,
types: Types,
declarations: Declarations,
implementations: Implementations,
properties: Properties,
}
impl GlobalCache {
fn extend(&mut self, other: Self) {
self.definitions.extend(other.definitions);
self.types.extend(other.types);
self.declarations.extend(other.declarations);
self.implementations.extend(other.implementations);
self.properties.extend(other.properties);
}
}
pub struct SolangServer {
client: Client,
target: Target,
importpaths: Vec<PathBuf>,
importmaps: Vec<(String, PathBuf)>,
files: Mutex<Files>,
global_cache: Mutex<GlobalCache>,
}
#[tokio::main(flavor = "current_thread")]
pub async fn start_server(language_args: &LanguageServerCommand) -> ! {
let mut importpaths = Vec::new();
let mut importmaps = Vec::new();
if let Some(paths) = &language_args.import_path {
for path in paths {
importpaths.push(path.clone());
}
}
if let Some(maps) = &language_args.import_map {
for (map, path) in maps {
importmaps.push((map.clone(), path.clone()));
}
}
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let target = target_arg(&language_args.target);
let (service, socket) = LspService::new(|client| SolangServer {
client,
target,
importpaths,
importmaps,
files: Mutex::new(Default::default()),
global_cache: Mutex::new(Default::default()),
});
Server::new(stdin, stdout, socket).serve(service).await;
std::process::exit(1);
}
impl SolangServer {
async fn parse_file(&self, uri: Url) {
let mut resolver = FileResolver::default();
for (path, contents) in &self.files.lock().await.text_buffers {
resolver.set_file_contents(path.to_str().unwrap(), contents.clone());
}
if let Ok(path) = uri.to_file_path() {
let dir = path.parent().unwrap();
resolver.add_import_path(dir);
let mut diags = Vec::new();
for path in &self.importpaths {
resolver.add_import_path(path);
}
for (map, path) in &self.importmaps {
resolver.add_import_map(OsString::from(map), PathBuf::from(path));
}
let os_str = path.file_name().unwrap();
let mut ns = parse_and_resolve(os_str, &mut resolver, self.target);
codegen(&mut ns, &Default::default());
diags.extend(ns.diagnostics.iter().filter_map(|diag| {
if diag.loc.file_no() != ns.top_file_no() {
return None;
}
let severity = match diag.level {
ast::Level::Info => Some(DiagnosticSeverity::INFORMATION),
ast::Level::Warning => Some(DiagnosticSeverity::WARNING),
ast::Level::Error => Some(DiagnosticSeverity::ERROR),
ast::Level::Debug => {
return None;
}
};
let related_information = if diag.notes.is_empty() {
None
} else {
Some(
diag.notes
.iter()
.map(|note| DiagnosticRelatedInformation {
message: note.message.to_string(),
location: Location {
uri: Url::from_file_path(&ns.files[note.loc.file_no()].path)
.unwrap(),
range: loc_to_range(¬e.loc, &ns.files[ns.top_file_no()]),
},
})
.collect(),
)
};
let range = loc_to_range(&diag.loc, &ns.files[ns.top_file_no()]);
Some(Diagnostic {
range,
message: diag.message.to_string(),
severity,
related_information,
..Default::default()
})
}));
let res = self.client.publish_diagnostics(uri, diags, None);
let (file_caches, global_cache) = Builder::new(&ns).build();
let mut files = self.files.lock().await;
for (f, c) in ns.files.iter().zip(file_caches.into_iter()) {
if f.cache_no.is_some() {
files.caches.insert(f.path.clone(), c);
}
}
let mut gc = self.global_cache.lock().await;
gc.extend(global_cache);
res.await;
}
}
async fn get_reference_from_params(
&self,
params: GotoDefinitionParams,
) -> Result<Option<DefinitionIndex>> {
let uri = params.text_document_position_params.text_document.uri;
let path = uri.to_file_path().map_err(|_| Error {
code: ErrorCode::InvalidRequest,
message: format!("Received invalid URI: {uri}").into(),
data: None,
})?;
let files = self.files.lock().await;
if let Some(cache) = files.caches.get(&path) {
let f = &cache.file;
if let Some(offset) = f.get_offset(
params.text_document_position_params.position.line as _,
params.text_document_position_params.position.character as _,
) {
if let Some(reference) = cache
.references
.find(offset, offset + 1)
.min_by(|a, b| (a.stop - a.start).cmp(&(b.stop - b.start)))
{
return Ok(Some(reference.val.clone()));
}
}
}
Ok(None)
}
}
struct Builder<'a> {
hovers: Vec<(usize, HoverEntry)>,
references: Vec<(usize, ReferenceEntry)>,
scopes: Vec<(usize, ScopeEntry)>,
top_level_code_objects: Vec<(usize, (String, Option<DefinitionIndex>))>,
definitions: Definitions,
types: Types,
declarations: Declarations,
implementations: Implementations,
properties: Properties,
ns: &'a ast::Namespace,
}
impl<'a> Builder<'a> {
fn new(ns: &'a ast::Namespace) -> Self {
Self {
hovers: Vec::new(),
references: Vec::new(),
scopes: Vec::new(),
top_level_code_objects: Vec::new(),
definitions: HashMap::new(),
types: HashMap::new(),
declarations: HashMap::new(),
implementations: HashMap::new(),
properties: HashMap::new(),
ns,
}
}
fn statement(&mut self, stmt: &ast::Statement, symtab: &symtable::Symtable) {
match stmt {
ast::Statement::Block { statements, .. } => {
for stmt in statements {
self.statement(stmt, symtab);
}
}
ast::Statement::VariableDecl(loc, var_no, param, expr) => {
if let Some(exp) = expr {
self.expression(exp, symtab);
}
let constant = self
.ns
.var_constants
.get(loc)
.and_then(get_constants)
.map(|s| format!(" = {s}"))
.unwrap_or_default();
let readonly = symtab
.vars
.get(var_no)
.map(|var| {
if var.slice {
"\nreadonly: compiled to slice\n"
} else {
""
}
})
.unwrap_or_default();
let val = format!(
"{} {}{}{}",
param.ty.to_string(self.ns),
param.name_as_str(),
constant,
readonly
);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: param.loc.start(),
stop: param.loc.exclusive_end(),
val: make_code_block(val),
},
));
if let Some(id) = ¶m.id {
let file_no = id.loc.file_no();
let file = &self.ns.files[file_no];
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Variable(*var_no),
};
self.definitions
.insert(di.clone(), loc_to_range(&id.loc, file));
if let Some(dt) = get_type_definition(¶m.ty) {
self.types.insert(di, dt.into());
}
}
if let Some(loc) = param.ty_loc {
if let Some(dt) = get_type_definition(¶m.ty) {
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: dt.into(),
},
));
}
}
}
ast::Statement::If(_, _, expr, stat1, stat2) => {
self.expression(expr, symtab);
for stmt in stat1 {
self.statement(stmt, symtab);
}
for stmt in stat2 {
self.statement(stmt, symtab);
}
}
ast::Statement::While(_, _, expr, block) => {
self.expression(expr, symtab);
for stmt in block {
self.statement(stmt, symtab);
}
}
ast::Statement::For {
init,
cond,
next,
body,
..
} => {
if let Some(exp) = cond {
self.expression(exp, symtab);
}
for stat in init {
self.statement(stat, symtab);
}
if let Some(exp) = next {
self.expression(exp, symtab);
}
for stat in body {
self.statement(stat, symtab);
}
}
ast::Statement::DoWhile(_, _, stat1, expr) => {
self.expression(expr, symtab);
for st1 in stat1 {
self.statement(st1, symtab);
}
}
ast::Statement::Expression(_, _, expr) => {
self.expression(expr, symtab);
}
ast::Statement::Delete(_, _, expr) => {
self.expression(expr, symtab);
}
ast::Statement::Destructure(_, fields, expr) => {
self.expression(expr, symtab);
for field in fields {
match field {
ast::DestructureField::Expression(expr) => {
self.expression(expr, symtab);
}
ast::DestructureField::VariableDecl(var_no, param) => {
self.hovers.push((
param.loc.file_no(),
HoverEntry {
start: param.loc.start(),
stop: param.loc.exclusive_end(),
val: self.expanded_ty(¶m.ty),
},
));
if let Some(id) = ¶m.id {
let file_no = id.loc.file_no();
let file = &self.ns.files[file_no];
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Variable(*var_no),
};
self.definitions
.insert(di.clone(), loc_to_range(&id.loc, file));
if let Some(dt) = get_type_definition(¶m.ty) {
self.types.insert(di, dt.into());
}
}
}
ast::DestructureField::None => (),
}
}
}
ast::Statement::Continue(_) => {}
ast::Statement::Break(_) => {}
ast::Statement::Return(_, None) => {}
ast::Statement::Return(_, Some(expr)) => {
self.expression(expr, symtab);
}
ast::Statement::Revert { args, .. } => {
for arg in args {
self.expression(arg, symtab);
}
}
ast::Statement::Emit {
event_no,
event_loc,
args,
..
} => {
let event = &self.ns.events[*event_no];
let mut tags = render(&event.tags);
if !tags.is_empty() {
tags.push_str("\n\n");
}
let fields = event
.fields
.iter()
.map(|field| {
format!(
"\t{}{}{}",
field.ty.to_string(self.ns),
if field.indexed { " indexed " } else { " " },
field.name_as_str()
)
})
.join(",\n");
let val = format!(
"event {} {{\n{}\n}}{}",
event.symbol_name(self.ns),
fields,
if event.anonymous { " anonymous" } else { "" }
);
self.hovers.push((
event_loc.file_no(),
HoverEntry {
start: event_loc.start(),
stop: event_loc.exclusive_end(),
val: format!("{}{}", tags, make_code_block(val)),
},
));
self.references.push((
event_loc.file_no(),
ReferenceEntry {
start: event_loc.start(),
stop: event_loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Event(*event_no),
},
},
));
for arg in args {
self.expression(arg, symtab);
}
}
ast::Statement::TryCatch(_, _, try_stmt) => {
self.expression(&try_stmt.expr, symtab);
if let Some(clause) = try_stmt.catch_all.as_ref() {
for stmt in &clause.stmt {
self.statement(stmt, symtab);
}
}
for stmt in &try_stmt.ok_stmt {
self.statement(stmt, symtab);
}
for clause in &try_stmt.errors {
for stmts in &clause.stmt {
self.statement(stmts, symtab);
}
}
}
ast::Statement::Underscore(_loc) => {}
ast::Statement::Assembly(..) => {
}
}
}
fn expression(&mut self, expr: &ast::Expression, symtab: &symtable::Symtable) {
match expr {
ast::Expression::BoolLiteral { loc, .. } => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block("bool"),
},
));
}
ast::Expression::BytesLiteral { loc, ty, .. } => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: self.expanded_ty(ty),
},
));
}
ast::Expression::NumberLiteral { loc, ty, value,.. } => {
if let Type::Enum(id) = ty {
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Variant(*id, value.to_u64().unwrap() as _),
},
},
));
}
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(ty.to_string(self.ns)),
}
));
}
ast::Expression::StructLiteral { id: id_path, ty, values, .. } => {
if let Type::Struct(StructType::UserDefined(id)) = ty {
let loc = id_path.identifiers.last().unwrap().loc;
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Struct(StructType::UserDefined(*id)),
},
},
));
}
for (i, (field_name, expr)) in values.iter().enumerate() {
self.expression(expr, symtab);
if let Some(pt::Identifier { loc: field_name_loc, ..}) = field_name {
self.references.push((
field_name_loc.file_no(),
ReferenceEntry {
start: field_name_loc.start(),
stop: field_name_loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Field(ty.clone(), i),
},
},
));
}
}
}
ast::Expression::ArrayLiteral { values, .. }
| ast::Expression::ConstArrayLiteral { values, .. } => {
for expr in values {
self.expression(expr, symtab);
}
}
ast::Expression::Add {
loc,
ty,
unchecked,
left,
right,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!(
"{} {} addition",
if *unchecked { "unchecked " } else { "" },
ty.to_string(self.ns)
),
},
));
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Subtract {
loc,
ty,
unchecked,
left,
right,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!(
"{} {} subtraction",
if *unchecked { "unchecked " } else { "" },
ty.to_string(self.ns)
),
}
));
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Multiply {
loc,
ty,
unchecked,
left,
right,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!(
"{} {} multiply",
if *unchecked { "unchecked " } else { "" },
ty.to_string(self.ns)
),
},
));
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Divide {
loc,
ty,
left,
right,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!("{} divide", ty.to_string(self.ns)),
},
));
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Modulo {
loc,
ty,
left,
right,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!("{} modulo", ty.to_string(self.ns)),
},
));
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Power {
loc,
ty,
unchecked,
base,
exp,
} => {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!(
"{} {}power",
if *unchecked { "unchecked " } else { "" },
ty.to_string(self.ns)
),
},
));
self.expression(base, symtab);
self.expression(exp, symtab);
}
ast::Expression::BitwiseOr { left, right, .. }
| ast::Expression::BitwiseAnd { left, right, .. }
| ast::Expression::BitwiseXor { left, right, .. }
| ast::Expression::ShiftLeft { left, right, .. }
| ast::Expression::ShiftRight { left, right, .. }
| ast::Expression::Or { left, right, .. }
| ast::Expression::And { left, right, .. }
| ast::Expression::Equal { left, right, .. }
| ast::Expression::More { left, right, .. }
| ast::Expression::MoreEqual { left, right, .. }
| ast::Expression::Less { left, right, .. }
| ast::Expression::LessEqual { left, right, .. }
| ast::Expression::NotEqual { left, right, .. }
| ast::Expression::Assign { left, right, .. }
=> {
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Variable { loc, ty, var_no } => {
let name = if let Some(var) = symtab.vars.get(var_no) {
&var.id.name
} else {
""
};
let readonly = symtab
.vars
.get(var_no)
.map(|var| {
if var.slice {
"\nreadonly: compiled to slice\n"
} else {
""
}
})
.unwrap_or_default();
let val = format!("{} {}{}", ty.to_string(self.ns), name, readonly);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(val),
},
));
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Variable(*var_no),
},
},
));
}
ast::Expression::ConstantVariable { loc, ty, contract_no, var_no } => {
let (contract, name) = if let Some(contract_no) = contract_no {
let contract = format!("{}.", self.ns.contracts[*contract_no].id);
let name = &self.ns.contracts[*contract_no].variables[*var_no].name;
(contract, name)
} else {
let contract = String::new();
let name = &self.ns.constants[*var_no].name;
(contract, name)
};
let constant = self
.ns
.var_constants
.get(loc)
.and_then(get_constants)
.map(|s| format!(" = {s}"))
.unwrap_or_default();
let val = format!("{} constant {}{}{}", ty.to_string(self.ns), contract, name, constant);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(val),
},
));
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::NonLocalVariable(*contract_no, *var_no),
},
},
));
}
ast::Expression::StorageVariable { loc, ty, contract_no, var_no } => {
let contract = &self.ns.contracts[*contract_no];
let name = &contract.variables[*var_no].name;
let val = format!("{} {}.{}", ty.to_string(self.ns), contract.id, name);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(val),
},
));
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::NonLocalVariable(Some(*contract_no), *var_no),
},
},
));
}
ast::Expression::Load { expr, .. }
| ast::Expression::StorageLoad { expr, .. }
| ast::Expression::ZeroExt { expr, .. }
| ast::Expression::SignExt { expr, .. }
| ast::Expression::Trunc { expr, .. }
| ast::Expression::Cast { expr, .. }
| ast::Expression::BytesCast { expr, .. }
| ast::Expression::PreIncrement { expr, .. }
| ast::Expression::PreDecrement { expr, .. }
| ast::Expression::PostIncrement { expr, .. }
| ast::Expression::PostDecrement { expr, .. }
| ast::Expression::Not { expr, .. }
| ast::Expression::BitwiseNot { expr, .. }
| ast::Expression::Negate { expr, .. } => {
self.expression(expr, symtab);
}
ast::Expression::ConditionalOperator {
cond,
true_option: left,
false_option: right,
..
} => {
self.expression(cond, symtab);
self.expression(left, symtab);
self.expression(right, symtab);
}
ast::Expression::Subscript { array, index, .. } => {
self.expression(array, symtab);
self.expression(index, symtab);
}
ast::Expression::StructMember { loc, expr, field, ty } => {
self.expression(expr, symtab);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(ty.to_string(self.ns)),
},
));
let t = expr.ty().deref_any().clone();
if let Type::Struct(StructType::UserDefined(_)) = t {
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Field(t, *field),
},
},
));
}
}
ast::Expression::AllocDynamicBytes { loc, ty, length, .. } => {
if let Some(dt) = get_type_definition(ty) {
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: dt.into(),
},
));
}
self.expression(length, symtab);
}
ast::Expression::StorageArrayLength { array, .. } => {
self.expression(array, symtab);
}
ast::Expression::StringCompare { left, right, .. } => {
if let ast::StringLocation::RunTime(expr) = left {
self.expression(expr, symtab);
}
if let ast::StringLocation::RunTime(expr) = right {
self.expression(expr, symtab);
}
}
ast::Expression::InternalFunction {id, function_no, ..} => {
let fnc = &self.ns.functions[*function_no];
let mut msg_tg = render(&fnc.tags[..]);
if !msg_tg.is_empty() {
msg_tg.push_str("\n\n");
}
let params = fnc.params.iter().map(|parm| format!("{} {}", parm.ty.to_string(self.ns), parm.name_as_str())).join(", ");
let rets = fnc.returns.iter().map(|ret| {
let mut msg = ret.ty.to_string(self.ns);
if ret.name_as_str() != "" {
msg = format!("{} {}", msg, ret.name_as_str());
}
msg
}).join(", ");
let contract = fnc.contract_no.map(|contract_no| format!("{}.", self.ns.contracts[contract_no].id)).unwrap_or_default();
let val = format!("{} {}{}({}) returns ({})\n", fnc.ty, contract, fnc.id, params, rets);
let func_loc = id.identifiers.last().unwrap().loc;
self.hovers.push((
func_loc.file_no(),
HoverEntry {
start: func_loc.start(),
stop: func_loc.exclusive_end(),
val: format!("{}{}", msg_tg, make_code_block(val)),
},
));
self.references.push((
func_loc.file_no(),
ReferenceEntry {
start: func_loc.start(),
stop: func_loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Function(*function_no),
},
},
));
}
ast::Expression::InternalFunctionCall {
function,
args,
..
} => {
if let ast::Expression::InternalFunction { .. } = function.as_ref() {
self.expression(function, symtab);
}
for arg in args {
self.expression(arg, symtab);
}
}
ast::Expression::ExternalFunction { loc, address, function_no, .. } => {
let fnc = &self.ns.functions[*function_no];
let mut msg_tg = render(&fnc.tags[..]);
if !msg_tg.is_empty() {
msg_tg.push_str("\n\n");
}
let params = fnc.params.iter().map(|parm| format!("{} {}", parm.ty.to_string(self.ns), parm.name_as_str())).join(", ");
let rets = fnc.returns.iter().map(|ret| {
let mut msg = ret.ty.to_string(self.ns);
if ret.name_as_str() != "" {
msg = format!("{} {}", msg, ret.name_as_str());
}
msg
}).join(", ");
let contract = fnc.contract_no.map(|contract_no| format!("{}.", self.ns.contracts[contract_no].id)).unwrap_or_default();
let val = format!("{} {}{}({}) returns ({})\n", fnc.ty, contract, fnc.id, params, rets);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!("{}{}", msg_tg, make_code_block(val)),
},
));
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Function(*function_no),
},
},
));
self.expression(address, symtab);
}
ast::Expression::ExternalFunctionCall {
function,
args,
call_args,
..
} => {
if let ast::Expression::ExternalFunction { .. } = function.as_ref() {
self.expression(function, symtab);
}
for arg in args {
self.expression(arg, symtab);
}
if let Some(value) = &call_args.value {
self.expression(value, symtab);
}
if let Some(gas) = &call_args.gas {
self.expression(gas, symtab);
}
}
ast::Expression::ExternalFunctionCallRaw {
address,
args,
call_args,
..
} => {
self.expression(args, symtab);
self.expression(address, symtab);
if let Some(value) = &call_args.value {
self.expression(value, symtab);
}
if let Some(gas) = &call_args.gas {
self.expression(gas, symtab);
}
}
ast::Expression::Constructor {
args, call_args, ..
} => {
if let Some(gas) = &call_args.gas {
self.expression(gas, symtab);
}
for arg in args {
self.expression(arg, symtab);
}
if let Some(optval) = &call_args.value {
self.expression(optval, symtab);
}
if let Some(optsalt) = &call_args.salt {
self.expression(optsalt, symtab);
}
if let Some(seeds) = &call_args.seeds {
self.expression(seeds, symtab);
}
}
ast::Expression::Builtin { loc, kind, args, .. } => {
let (rets, name, params, doc) = if let Some(protval) = get_prototype(*kind) {
let rets = protval.ret.iter().map(|ret| ret.to_string(self.ns)).join(" ");
let mut params = protval.params.iter().map(|param| param.to_string(self.ns)).join(" ");
if !params.is_empty() {
params = format!("({params})");
}
let doc = format!("{}\n\n", protval.doc);
(rets, protval.name, params, doc)
} else {
(String::new(), "", String::new(), String::new())
};
let val = make_code_block(format!("[built-in] {rets} {name} {params}"));
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!("{doc}{val}"),
},
));
for expr in args {
self.expression(expr, symtab);
}
}
ast::Expression::FormatString {format, .. } => {
for (_, e) in format {
self.expression(e, symtab);
}
}
ast::Expression::List { list, .. } => {
for expr in list {
self.expression(expr, symtab);
}
}
_ => {}
}
}
fn contract_variable(
&mut self,
variable: &ast::Variable,
symtab: &symtable::Symtable,
contract_no: Option<usize>,
var_no: usize,
) {
let mut tags = render(&variable.tags[..]);
if !tags.is_empty() {
tags.push_str("\n\n");
}
let val = make_code_block(format!(
"{} {}",
variable.ty.to_string(self.ns),
variable.name
));
if let Some(expr) = &variable.initializer {
self.expression(expr, symtab);
}
let file_no = variable.loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: variable.loc.start(),
stop: variable.loc.start() + variable.name.len(),
val: format!("{tags}{val}"),
},
));
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::NonLocalVariable(contract_no, var_no),
};
self.definitions
.insert(di.clone(), loc_to_range(&variable.loc, file));
if let Some(dt) = get_type_definition(&variable.ty) {
self.types.insert(di, dt.into());
}
if contract_no.is_none() {
self.top_level_code_objects.push((
file_no,
(
variable.name.clone(),
get_type_definition(&variable.ty).map(|dt| dt.into()),
),
))
}
}
fn field(&mut self, id: usize, field_id: usize, field: &ast::Parameter<Type>) {
if let Some(loc) = field.ty_loc {
if let Some(dt) = get_type_definition(&field.ty) {
self.references.push((
loc.file_no(),
ReferenceEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: dt.into(),
},
));
}
}
let loc = field.id.as_ref().map(|id| &id.loc).unwrap_or(&field.loc);
let file_no = loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(format!(
"{} {}",
field.ty.to_string(self.ns),
field.name_as_str()
)),
},
));
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Field(
Type::Struct(ast::StructType::UserDefined(id)),
field_id,
),
};
self.definitions.insert(di.clone(), loc_to_range(loc, file));
if let Some(dt) = get_type_definition(&field.ty) {
self.types.insert(di, dt.into());
}
}
fn build(mut self) -> (Vec<FileCache>, GlobalCache) {
for (ei, enum_decl) in self.ns.enums.iter().enumerate() {
for (discriminant, (nam, loc)) in enum_decl.values.iter().enumerate() {
let file_no = loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: make_code_block(format!(
"enum {}.{} {}",
enum_decl.id, nam, discriminant
)),
},
));
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Variant(ei, discriminant),
};
self.definitions.insert(di.clone(), loc_to_range(loc, file));
let dt = DefinitionType::Enum(ei);
self.types.insert(di, dt.into());
}
let file_no = enum_decl.id.loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: enum_decl.id.loc.start(),
stop: enum_decl.id.loc.exclusive_end(),
val: render(&enum_decl.tags[..]),
},
));
let def_index = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Enum(ei),
};
self.definitions
.insert(def_index.clone(), loc_to_range(&enum_decl.id.loc, file));
self.properties.insert(
def_index.clone(),
enum_decl
.values
.iter()
.map(|(name, _)| (name.clone(), None))
.collect(),
);
if enum_decl.contract.is_none() {
self.top_level_code_objects
.push((file_no, (enum_decl.id.name.clone(), Some(def_index))));
}
}
for (si, struct_decl) in self.ns.structs.iter().enumerate() {
if matches!(struct_decl.loc, pt::Loc::File(_, _, _)) {
for (fi, field) in struct_decl.fields.iter().enumerate() {
self.field(si, fi, field);
}
let file_no = struct_decl.id.loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: struct_decl.id.loc.start(),
stop: struct_decl.id.loc.exclusive_end(),
val: render(&struct_decl.tags[..]),
},
));
let def_index = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Struct(StructType::UserDefined(si)),
};
self.definitions
.insert(def_index.clone(), loc_to_range(&struct_decl.id.loc, file));
self.properties.insert(
def_index.clone(),
struct_decl
.fields
.iter()
.filter_map(|field| {
let def_index =
get_type_definition(&field.ty).map(|def_type| DefinitionIndex {
def_path: file.path.clone(),
def_type,
});
field.id.as_ref().map(|id| (id.name.clone(), def_index))
})
.collect(),
);
if struct_decl.contract.is_none() {
self.top_level_code_objects
.push((file_no, (struct_decl.id.name.clone(), Some(def_index))));
}
}
}
for (i, func) in self.ns.functions.iter().enumerate() {
if func.is_accessor || func.loc == pt::Loc::Builtin {
continue;
}
if let Some(bump) = &func.annotations.bump {
self.expression(&bump.1, &func.symtable);
}
for seed in &func.annotations.seeds {
self.expression(&seed.1, &func.symtable);
}
if let Some(space) = &func.annotations.space {
self.expression(&space.1, &func.symtable);
}
if let Some((loc, name)) = &func.annotations.payer {
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: format!("payer account: {name}"),
},
));
}
for (i, param) in func.params.iter().enumerate() {
let loc = param.id.as_ref().map(|id| &id.loc).unwrap_or(¶m.loc);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: self.expanded_ty(¶m.ty),
},
));
if let Some(Some(var_no)) = func.symtable.arguments.get(i) {
if let Some(id) = ¶m.id {
let file_no = id.loc.file_no();
let file = &self.ns.files[file_no];
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Variable(*var_no),
};
self.definitions
.insert(di.clone(), loc_to_range(&id.loc, file));
if let Some(dt) = get_type_definition(¶m.ty) {
self.types.insert(di, dt.into());
}
}
}
if let Some(ty_loc) = param.ty_loc {
if let Some(dt) = get_type_definition(¶m.ty) {
self.references.push((
ty_loc.file_no(),
ReferenceEntry {
start: ty_loc.start(),
stop: ty_loc.exclusive_end(),
val: dt.into(),
},
));
}
}
}
for (i, ret) in func.returns.iter().enumerate() {
let loc = ret.id.as_ref().map(|id| &id.loc).unwrap_or(&ret.loc);
self.hovers.push((
loc.file_no(),
HoverEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: self.expanded_ty(&ret.ty),
},
));
if let Some(id) = &ret.id {
if let Some(var_no) = func.symtable.returns.get(i) {
let file_no = id.loc.file_no();
let file = &self.ns.files[file_no];
let di = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Variable(*var_no),
};
self.definitions
.insert(di.clone(), loc_to_range(&id.loc, file));
if let Some(dt) = get_type_definition(&ret.ty) {
self.types.insert(di, dt.into());
}
}
}
if let Some(ty_loc) = ret.ty_loc {
if let Some(dt) = get_type_definition(&ret.ty) {
self.references.push((
ty_loc.file_no(),
ReferenceEntry {
start: ty_loc.start(),
stop: ty_loc.exclusive_end(),
val: dt.into(),
},
));
}
}
}
for stmt in &func.body {
self.statement(stmt, &func.symtable);
}
let file_no = func.id.loc.file_no();
let file = &self.ns.files[file_no];
self.definitions.insert(
DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Function(i),
},
loc_to_range(&func.id.loc, file),
);
self.scopes.extend(func.symtable.scopes.iter().map(|scope| {
let loc = scope.loc.unwrap();
let scope_entry = ScopeEntry {
start: loc.start(),
stop: loc.exclusive_end(),
val: scope
.names
.values()
.filter_map(|pos| {
func.symtable.vars.get(pos).map(|var| {
(
var.id.name.clone(),
get_type_definition(&var.ty).map(|def_type| def_type.into()),
)
})
})
.collect_vec(),
};
(file_no, scope_entry)
}));
if func.contract_no.is_none() {
self.top_level_code_objects
.push((file_no, (func.id.name.clone(), None)))
}
}
for (i, constant) in self.ns.constants.iter().enumerate() {
let samptb = symtable::Symtable::default();
self.contract_variable(constant, &samptb, None, i);
}
for (ci, contract) in self.ns.contracts.iter().enumerate() {
for base in &contract.bases {
let file_no = base.loc.file_no();
self.hovers.push((
file_no,
HoverEntry {
start: base.loc.start(),
stop: base.loc.exclusive_end(),
val: make_code_block(format!(
"contract {}",
self.ns.contracts[base.contract_no].id
)),
},
));
self.references.push((
file_no,
ReferenceEntry {
start: base.loc.start(),
stop: base.loc.exclusive_end(),
val: DefinitionIndex {
def_path: Default::default(),
def_type: DefinitionType::Contract(base.contract_no),
},
},
));
}
for (i, variable) in contract.variables.iter().enumerate() {
let symtable = symtable::Symtable::default();
self.contract_variable(variable, &symtable, Some(ci), i);
}
let file_no = contract.loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: contract.id.loc.start(),
stop: contract.id.loc.exclusive_end(),
val: render(&contract.tags[..]),
},
));
let contract_def_index = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Contract(ci),
};
self.definitions.insert(
contract_def_index.clone(),
loc_to_range(&contract.id.loc, file),
);
let impls = contract
.functions
.iter()
.map(|f| DefinitionIndex {
def_path: file.path.clone(), def_type: DefinitionType::Function(*f),
})
.collect();
self.implementations
.insert(contract_def_index.clone(), impls);
let decls = contract
.virtual_functions
.iter()
.filter_map(|(_, indices)| {
let func = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Function(indices.last().copied().unwrap()),
};
let all_decls: HashSet<usize> = HashSet::from_iter(indices.iter().copied());
let parent_decls = contract
.bases
.iter()
.map(|b| {
let p = &self.ns.contracts[b.contract_no];
HashSet::from_iter(p.functions.iter().copied())
.intersection(&all_decls)
.copied()
.collect::<HashSet<usize>>()
})
.reduce(|acc, e| acc.union(&e).copied().collect());
parent_decls.map(|parent_decls| {
let decls = parent_decls
.iter()
.map(|&i| {
let loc = self.ns.functions[i].loc;
DefinitionIndex {
def_path: self.ns.files[loc.file_no()].path.clone(),
def_type: DefinitionType::Function(i),
}
})
.collect::<Vec<_>>();
(func, decls)
})
});
self.declarations.extend(decls);
let functions = contract.functions.iter().filter_map(|&fno| {
self.ns
.functions
.get(fno)
.map(|func| (func.id.name.clone(), None))
});
let structs = self
.ns
.structs
.iter()
.enumerate()
.filter_map(|(i, r#struct)| match &r#struct.contract {
Some(contract_name) if contract_name == &contract.id.name => Some((
r#struct.id.name.clone(),
Some(DefinitionType::Struct(StructType::UserDefined(i))),
)),
_ => None,
});
let enums =
self.ns
.enums
.iter()
.enumerate()
.filter_map(|(i, r#enum)| match &r#enum.contract {
Some(contract_name) if contract_name == &contract.id.name => {
Some((r#enum.id.name.clone(), Some(DefinitionType::Enum(i))))
}
_ => None,
});
let events =
self.ns
.events
.iter()
.enumerate()
.filter_map(|(i, event)| match &event.contract {
Some(event_contract) if *event_contract == ci => {
Some((event.id.name.clone(), Some(DefinitionType::Event(i))))
}
_ => None,
});
let variables = contract.variables.iter().map(|var| {
(
var.name.clone(),
get_type_definition(&var.ty).map(|def_type| def_type.into()),
)
});
let contract_contents = functions
.chain(structs)
.chain(enums)
.chain(events)
.map(|(name, dt)| {
let def_index = dt.map(|def_type| DefinitionIndex {
def_path: file.path.clone(),
def_type,
});
(name, def_index)
})
.chain(variables);
self.properties.insert(
contract_def_index.clone(),
contract_contents.clone().collect(),
);
self.scopes.push((
file_no,
ScopeEntry {
start: contract.loc.start(),
stop: contract.loc.exclusive_end(),
val: contract_contents.collect(),
},
));
self.top_level_code_objects.push((
file_no,
(contract.id.name.clone(), Some(contract_def_index)),
));
}
for (ei, event) in self.ns.events.iter().enumerate() {
for (fi, field) in event.fields.iter().enumerate() {
self.field(ei, fi, field);
}
let file_no = event.id.loc.file_no();
let file = &self.ns.files[file_no];
self.hovers.push((
file_no,
HoverEntry {
start: event.id.loc.start(),
stop: event.id.loc.exclusive_end(),
val: render(&event.tags[..]),
},
));
let def_index = DefinitionIndex {
def_path: file.path.clone(),
def_type: DefinitionType::Event(ei),
};
self.definitions
.insert(def_index.clone(), loc_to_range(&event.id.loc, file));
if event.contract.is_none() {
self.top_level_code_objects
.push((file_no, (event.id.name.clone(), Some(def_index))));
}
}
for lookup in &mut self.hovers {
if let Some(msg) =
self.ns
.hover_overrides
.get(&pt::Loc::File(lookup.0, lookup.1.start, lookup.1.stop))
{
lookup.1.val.clone_from(msg);
}
}
let defs_to_files = self
.definitions
.keys()
.map(|key| (key.def_type.clone(), key.def_path.clone()))
.collect::<HashMap<DefinitionType, PathBuf>>();
let defs_to_file_nos = self
.ns
.files
.iter()
.enumerate()
.map(|(i, f)| (f.path.clone(), i))
.collect::<HashMap<PathBuf, usize>>();
for val in self.types.values_mut() {
if let Some(path) = defs_to_files.get(&val.def_type) {
val.def_path.clone_from(path);
}
}
for (di, range) in &self.definitions {
if let Some(&file_no) = defs_to_file_nos.get(&di.def_path) {
let file = &self.ns.files[file_no];
self.references.push((
file_no,
ReferenceEntry {
start: file
.get_offset(range.start.line as usize, range.start.character as usize)
.unwrap(),
stop: file
.get_offset(range.end.line as usize, range.end.character as usize)
.unwrap()
+ 1,
val: di.clone(),
},
));
}
}
let file_caches = self
.ns
.files
.iter()
.enumerate()
.map(|(i, f)| FileCache {
file: f.clone(),
hovers: Lapper::new(
self.hovers
.iter()
.filter(|h| h.0 == i)
.map(|(_, i)| i.clone())
.collect(),
),
references: Lapper::new(
self.references
.iter()
.filter(|reference| reference.0 == i)
.map(|(_, i)| {
let mut i = i.clone();
if let Some(def_path) = defs_to_files.get(&i.val.def_type) {
i.val.def_path.clone_from(def_path);
}
i
})
.collect(),
),
scopes: Lapper::new(
self.scopes
.iter()
.filter(|scope| scope.0 == i)
.map(|(_, scope)| {
let mut scope = scope.clone();
for val in &mut scope.val {
if let Some(val) = &mut val.1 {
if let Some(def_path) = defs_to_files.get(&val.def_type) {
val.def_path.clone_from(def_path);
}
}
}
scope
})
.collect(),
),
top_level_code_objects: self
.top_level_code_objects
.iter_mut()
.filter(|code_object| code_object.0 == i)
.map(|code_object| {
if let Some(DefinitionIndex { def_path, def_type }) = &mut code_object.1 .1
{
if def_path.to_str().unwrap() == "" {
if let Some(dp) = defs_to_files.get(def_type) {
def_path.clone_from(dp);
}
}
}
code_object.1.clone()
})
.collect(),
})
.collect();
for properties in self.properties.values_mut() {
for def_index in properties.values_mut().flatten() {
if def_index.def_path.to_str().unwrap() == "" {
if let Some(def_path) = defs_to_files.get(&def_index.def_type) {
def_index.def_path.clone_from(def_path);
}
}
}
}
let global_cache = GlobalCache {
definitions: self.definitions,
types: self.types,
declarations: self.declarations,
implementations: self.implementations,
properties: self.properties,
};
(file_caches, global_cache)
}
fn expanded_ty(&self, ty: &ast::Type) -> String {
match ty {
ast::Type::Ref(ty) => self.expanded_ty(ty),
ast::Type::StorageRef(_, ty) => self.expanded_ty(ty),
ast::Type::Struct(struct_type) => {
let strct = struct_type.definition(self.ns);
let mut tags = render(&strct.tags);
if !tags.is_empty() {
tags.push_str("\n\n")
}
let fields = strct
.fields
.iter()
.map(|field| {
format!("\t{} {}", field.ty.to_string(self.ns), field.name_as_str())
})
.join(",\n");
let val = make_code_block(format!("struct {strct} {{\n{fields}\n}}"));
format!("{tags}{val}")
}
ast::Type::Enum(n) => {
let enm = &self.ns.enums[*n];
let mut tags = render(&enm.tags);
if !tags.is_empty() {
tags.push_str("\n\n")
}
let values = enm
.values
.iter()
.map(|value| format!("\t{}", value.0))
.join(",\n");
let val = make_code_block(format!("enum {enm} {{\n{values}\n}}"));
format!("{tags}{val}")
}
_ => make_code_block(ty.to_string(self.ns)),
}
}
}
#[tower_lsp::async_trait]
impl LanguageServer for SolangServer {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
server_info: None,
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(false),
trigger_characters: Some(vec![".".to_string()]),
all_commit_characters: None,
work_done_progress_options: Default::default(),
completion_item: None,
}),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: None,
retrigger_characters: None,
work_done_progress_options: Default::default(),
}),
document_highlight_provider: None,
workspace_symbol_provider: Some(OneOf::Left(true)),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec![],
work_done_progress_options: Default::default(),
}),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
definition_provider: Some(OneOf::Left(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
references_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
..ServerCapabilities::default()
},
})
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(
MessageType::INFO,
format!(
"solang language server {} initialized",
env!("SOLANG_VERSION")
),
)
.await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
self.client
.log_message(MessageType::INFO, "workspace folders changed!")
.await;
}
async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
self.client
.log_message(MessageType::INFO, "configuration changed!")
.await;
}
async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
self.client
.log_message(MessageType::INFO, "watched files have changed!")
.await;
}
async fn execute_command(&self, _: ExecuteCommandParams) -> Result<Option<Value>> {
self.client
.log_message(MessageType::INFO, "command executed!")
.await;
Ok(None)
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
match uri.to_file_path() {
Ok(path) => {
self.files
.lock()
.await
.text_buffers
.insert(path, params.text_document.text);
self.parse_file(uri).await;
}
Err(_) => {
self.client
.log_message(MessageType::ERROR, format!("received invalid URI: {uri}"))
.await;
}
}
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
match uri.to_file_path() {
Ok(path) => {
if let Some(text_buf) = self.files.lock().await.text_buffers.get_mut(&path) {
*text_buf = params
.content_changes
.into_iter()
.fold(text_buf.clone(), update_file_contents);
}
self.parse_file(uri).await;
}
Err(_) => {
self.client
.log_message(MessageType::ERROR, format!("received invalid URI: {uri}"))
.await;
}
}
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri;
if let Some(text) = params.text {
if let Ok(path) = uri.to_file_path() {
if let Some(text_buf) = self.files.lock().await.text_buffers.get_mut(&path) {
*text_buf = text;
}
}
}
self.parse_file(uri).await;
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
if let Ok(path) = uri.to_file_path() {
let mut files = self.files.lock().await;
files.caches.remove(&path);
files.text_buffers.remove(&path);
}
self.client.publish_diagnostics(uri, vec![], None).await;
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let path = uri.to_file_path().map_err(|_| Error {
code: ErrorCode::InvalidRequest,
message: format!("Received invalid URI: {uri}").into(),
data: None,
})?;
let files = self.files.lock().await;
let Some(cache) = files.caches.get(&path) else {
return Ok(None);
};
let offset = cache
.file
.get_offset(
params.text_document_position.position.line as _,
params.text_document_position.position.character as _,
)
.unwrap();
let builtin_functions = BUILTIN_FUNCTIONS
.iter()
.filter(|function| function.target.is_empty() || function.target.contains(&self.target))
.map(|function| (function.name.to_string(), None));
let builtin_variables = BUILTIN_VARIABLE
.iter()
.filter(|var| var.target.is_empty() || var.target.contains(&self.target))
.map(|var| (var.name.to_string(), None));
let code_objects_in_scope = cache
.scopes
.find(offset, offset + 1)
.flat_map(|scope| scope.val.iter().cloned())
.chain(cache.top_level_code_objects.clone())
.chain(builtin_functions)
.chain(builtin_variables)
.collect::<HashMap<_, _>>();
let global_cache = self.global_cache.lock().await;
let suggestions = match params.context {
Some(CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(trigger_character),
}) if trigger_character == "." => {
let Some(text_buf) = files.text_buffers.get(&path) else {
return Ok(None);
};
let mut builtin_methods =
HashMap::<DefinitionType, HashMap<String, Option<DefinitionIndex>>>::new();
for method in BUILTIN_METHODS.iter().filter(|method| {
method.target.is_empty() || method.target.contains(&self.target)
}) {
if let Some(def_type) = get_type_definition(&method.method[0]) {
builtin_methods
.entry(def_type)
.or_default()
.insert(method.name.to_string(), None);
}
}
let builtin_structs = BUILTIN_STRUCTS
.iter()
.map(|r#struct| {
let def_type = DefinitionType::Struct(r#struct.struct_type);
let fields = r#struct
.struct_decl
.fields
.iter()
.map(|field| (field.name_as_str().to_string(), None))
.collect();
(def_type, fields)
})
.collect::<HashMap<_, HashMap<_, _>>>();
let code_object = {
let buffer = text_buf.chars().collect_vec();
let mut curr: isize = offset as isize - 2;
while curr >= 0
&& (buffer[curr as usize].is_ascii_alphanumeric()
|| buffer[curr as usize] == '.')
{
curr -= 1;
}
curr = isize::max(curr, 0);
if !buffer[curr as usize].is_ascii_alphanumeric() {
curr += 1;
}
let name = buffer[curr as usize..offset - 1].iter().collect::<String>();
name
};
let mut code_object_parts = code_object.split('.');
let properties = code_object_parts.next().and_then(|symbol| {
code_objects_in_scope
.get(symbol)
.and_then(|def_index| def_index.as_ref())
.and_then(|def_index| {
global_cache
.properties
.get(def_index)
.or_else(|| builtin_methods.get(&def_index.def_type))
.or_else(|| builtin_structs.get(&def_index.def_type))
})
});
let properties = code_object_parts.fold(properties, |acc, prop| {
acc.and_then(|properties| properties.get(prop))
.and_then(|def_index| def_index.as_ref())
.and_then(|def_index| {
global_cache
.properties
.get(def_index)
.or_else(|| builtin_methods.get(&def_index.def_type))
.or_else(|| builtin_structs.get(&def_index.def_type))
})
});
properties.map(|properties| {
properties
.keys()
.map(|name| CompletionItem {
label: name.clone(),
..Default::default()
})
.collect_vec()
})
}
Some(CompletionContext {
trigger_kind: CompletionTriggerKind::INVOKED,
..
}) => {
let suggestions = code_objects_in_scope
.into_keys()
.map(|label| CompletionItem {
label: label.clone(),
..Default::default()
})
.collect_vec();
Some(suggestions)
}
_ => None,
};
Ok(suggestions.map(CompletionResponse::Array))
}
async fn hover(&self, hverparam: HoverParams) -> Result<Option<Hover>> {
let txtdoc = hverparam.text_document_position_params.text_document;
let pos = hverparam.text_document_position_params.position;
let uri = txtdoc.uri;
if let Ok(path) = uri.to_file_path() {
let files = &self.files.lock().await;
if let Some(cache) = files.caches.get(&path) {
if let Some(offset) = cache
.file
.get_offset(pos.line as usize, pos.character as usize)
{
if let Some(hover) = cache
.hovers
.find(offset, offset + 1)
.min_by(|a, b| (a.stop - a.start).cmp(&(b.stop - b.start)))
{
let range = get_range_exclusive(hover.start, hover.stop, &cache.file);
return Ok(Some(Hover {
contents: HoverContents::Scalar(MarkedString::from_markdown(
hover.val.to_string(),
)),
range: Some(range),
}));
}
}
}
}
Ok(None)
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
let Some(reference) = self.get_reference_from_params(params).await? else {
return Ok(None);
};
let definitions = &self.global_cache.lock().await.definitions;
let location = definitions
.get(&reference)
.map(|range| {
let uri = Url::from_file_path(&reference.def_path).unwrap();
Location { uri, range: *range }
})
.map(GotoTypeDefinitionResponse::Scalar);
Ok(location)
}
async fn goto_type_definition(
&self,
params: GotoTypeDefinitionParams,
) -> Result<Option<GotoTypeDefinitionResponse>> {
let Some(reference) = self.get_reference_from_params(params).await? else {
return Ok(None);
};
let gc = self.global_cache.lock().await;
let di = match &reference.def_type {
DefinitionType::Variable(_)
| DefinitionType::NonLocalVariable(_, _)
| DefinitionType::Field(_, _)
| DefinitionType::Variant(_, _) => {
if let Some(def) = gc.types.get(&reference) {
def
} else {
return Ok(None);
}
}
DefinitionType::Struct(_)
| DefinitionType::Enum(_)
| DefinitionType::Contract(_)
| DefinitionType::Event(_)
| DefinitionType::UserType(_) => &reference,
_ => return Ok(None),
};
let location = gc
.definitions
.get(di)
.map(|range| {
let uri = Url::from_file_path(&di.def_path).unwrap();
Location { uri, range: *range }
})
.map(GotoTypeDefinitionResponse::Scalar);
Ok(location)
}
async fn goto_implementation(
&self,
params: GotoImplementationParams,
) -> Result<Option<GotoImplementationResponse>> {
let Some(reference) = self.get_reference_from_params(params).await? else {
return Ok(None);
};
let gc = self.global_cache.lock().await;
let impls = match &reference.def_type {
DefinitionType::Variable(_)
| DefinitionType::NonLocalVariable(_, _)
| DefinitionType::Field(_, _) => gc.types.get(&reference).and_then(|ty| {
if matches!(ty.def_type, DefinitionType::Contract(_)) {
gc.implementations.get(ty)
} else {
None
}
}),
DefinitionType::Contract(_) => gc.implementations.get(&reference),
_ => None,
};
let impls = impls
.map(|impls| {
impls
.iter()
.filter_map(|di| {
let path = &di.def_path;
gc.definitions.get(di).map(|range| {
let uri = Url::from_file_path(path).unwrap();
Location { uri, range: *range }
})
})
.collect()
})
.map(GotoImplementationResponse::Array);
Ok(impls)
}
async fn goto_declaration(
&self,
params: GotoDeclarationParams,
) -> Result<Option<GotoDeclarationResponse>> {
let Some(reference) = self.get_reference_from_params(params).await? else {
return Ok(None);
};
let gc = self.global_cache.lock().await;
let decls = gc.declarations.get(&reference);
let locations = decls
.map(|decls| {
decls
.iter()
.filter_map(|di| {
let path = &di.def_path;
gc.definitions.get(di).map(|range| {
let uri = Url::from_file_path(path).unwrap();
Location { uri, range: *range }
})
})
.collect()
})
.map(GotoImplementationResponse::Array);
Ok(locations)
}
async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
let def_params: GotoDefinitionParams = GotoDefinitionParams {
text_document_position_params: params.text_document_position,
work_done_progress_params: params.work_done_progress_params,
partial_result_params: params.partial_result_params,
};
let Some(reference) = self.get_reference_from_params(def_params).await? else {
return Ok(None);
};
let caches = &self.files.lock().await.caches;
let mut locations: Vec<_> = caches
.iter()
.flat_map(|(p, cache)| {
let uri = Url::from_file_path(p).unwrap();
cache
.references
.iter()
.filter(|r| r.val == reference)
.map(move |r| Location {
uri: uri.clone(),
range: get_range_exclusive(r.start, r.stop, &cache.file),
})
})
.collect();
if !params.context.include_declaration {
let definitions = &self.global_cache.lock().await.definitions;
let uri = Url::from_file_path(&reference.def_path).unwrap();
if let Some(range) = definitions.get(&reference) {
let def = Location { uri, range: *range };
locations.retain(|loc| loc != &def);
}
}
let locations = if locations.is_empty() {
None
} else {
Some(locations)
};
Ok(locations)
}
async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
let def_params: GotoDefinitionParams = GotoDefinitionParams {
text_document_position_params: params.text_document_position,
work_done_progress_params: params.work_done_progress_params,
partial_result_params: Default::default(),
};
let Some(reference) = self.get_reference_from_params(def_params).await? else {
return Ok(None);
};
let new_text = params.new_name;
let caches = &self.files.lock().await.caches;
let ws = caches
.iter()
.map(|(p, cache)| {
let uri = Url::from_file_path(p).unwrap();
let text_edits: Vec<_> = cache
.references
.iter()
.filter(|r| r.val == reference)
.map(|r| TextEdit {
range: get_range_exclusive(r.start, r.stop, &cache.file),
new_text: new_text.clone(),
})
.collect();
(uri, text_edits)
})
.collect::<HashMap<_, _>>();
Ok(Some(WorkspaceEdit::new(ws)))
}
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let source_path = uri.to_file_path().map_err(|_| Error {
code: ErrorCode::InvalidRequest,
message: format!("Received invalid URI: {uri}").into(),
data: None,
})?;
let source = std::fs::read_to_string(source_path).map_err(|err| Error {
code: ErrorCode::InternalError,
message: format!("Failed to read file: {uri}").into(),
data: Some(Value::String(format!("{:?}", err))),
})?;
let source_parsed = parse(&source).map_err(|err| {
let err = err
.into_iter()
.map(|e| Value::String(e.message))
.collect::<Vec<_>>();
Error {
code: ErrorCode::InternalError,
message: format!("Failed to parse file: {uri}").into(),
data: Some(Value::Array(err)),
}
})?;
let config = FormatterConfig {
line_length: 80,
tab_width: params.options.tab_size as _,
..Default::default()
};
let mut source_formatted = String::new();
format_to(&mut source_formatted, source_parsed, config).map_err(|err| Error {
code: ErrorCode::InternalError,
message: format!("Failed to format file: {uri}").into(),
data: Some(Value::String(format!("{:?}", err))),
})?;
let text_edit = TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: u32::MAX,
character: u32::MAX,
},
},
new_text: source_formatted,
};
Ok(Some(vec![text_edit]))
}
}
fn loc_to_range(loc: &pt::Loc, file: &ast::File) -> Range {
get_range(loc.start(), loc.end(), file)
}
fn get_range(start: usize, end: usize, file: &ast::File) -> Range {
let (line, column) = file.offset_to_line_column(start);
let start = Position::new(line as u32, column as u32);
let (line, column) = file.offset_to_line_column(end);
let end = Position::new(line as u32, column as u32);
Range::new(start, end)
}
fn get_range_exclusive(start: usize, end: usize, file: &ast::File) -> Range {
get_range(start, end - 1, file)
}
fn get_type_definition(ty: &Type) -> Option<DefinitionType> {
match ty {
Type::Enum(id) => Some(DefinitionType::Enum(*id)),
Type::Struct(st) => Some(DefinitionType::Struct(*st)),
Type::Array(ty, _) => get_type_definition(ty),
Type::Ref(ty) => get_type_definition(ty),
Type::StorageRef(_, ty) => get_type_definition(ty),
Type::Contract(id) => Some(DefinitionType::Contract(*id)),
Type::UserType(id) => Some(DefinitionType::UserType(*id)),
Type::DynamicBytes => Some(DefinitionType::DynamicBytes),
_ => None,
}
}
fn make_code_block(s: impl AsRef<str>) -> String {
format!("```solidity\n{}\n```", s.as_ref())
}
fn get_constants(expr: &Expression) -> Option<String> {
let val = match expr {
codegen::Expression::BytesLiteral {
ty: ast::Type::Bytes(_) | ast::Type::DynamicBytes,
value,
..
} => {
format!("hex\"{}\"", hex::encode(value))
}
codegen::Expression::BytesLiteral {
ty: ast::Type::String,
value,
..
} => {
format!("\"{}\"", String::from_utf8_lossy(value))
}
codegen::Expression::NumberLiteral {
ty: ast::Type::Uint(_) | ast::Type::Int(_),
value,
..
} => {
format!("{value}")
}
_ => return None,
};
Some(val)
}
fn update_file_contents(
mut prev_content: String,
content_change: TextDocumentContentChangeEvent,
) -> String {
if let Some(range) = content_change.range {
let start_line = range.start.line as usize;
let start_col = range.start.character as usize;
let end_line = range.end.line as usize;
let end_col = range.end.character as usize;
if start_line == prev_content.lines().count() {
prev_content.push_str(&content_change.text);
return prev_content;
}
let mut new_content = String::new();
for (i, line) in prev_content.lines().enumerate() {
if i < start_line {
new_content.push_str(line);
new_content.push('\n');
continue;
}
if i > end_line {
new_content.push_str(line);
new_content.push('\n');
continue;
}
if i == start_line {
new_content.push_str(&line[..start_col]);
new_content.push_str(&content_change.text);
}
if i == end_line {
new_content.push_str(&line[end_col..]);
new_content.push('\n');
}
}
new_content
} else {
content_change.text
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn without_range() {
let initial_content = "contract foo {\n function bar(Book y, Book x) public returns (bool) {\n return y.available;\n }\n}\n".to_string();
let new_content = "struct Book {\n string name;\n string writer;\n uint id;\n bool available;\n}\n".to_string();
assert_eq!(
new_content.clone(),
update_file_contents(
initial_content,
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: new_content
}
)
);
}
#[test]
fn at_the_end_of_file() {
let initial_content = "contract foo {\n function bar(Book y, Book x) public returns (bool) {\n return y.available;\n }\n}\n".to_string();
let new_content = "struct Book {\n string name;\n string writer;\n uint id;\n bool available;\n}\n".to_string();
let final_content = "\
contract foo {\n function bar(Book y, Book x) public returns (bool) {\n return y.available;\n }\n}\n\
struct Book {\n string name;\n string writer;\n uint id;\n bool available;\n}\n\
".to_string();
assert_eq!(
final_content,
update_file_contents(
initial_content,
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 5,
character: 0
},
end: Position {
line: 5,
character: 0
}
}),
range_length: Some(0),
text: new_content
}
),
);
}
#[test]
fn remove_content() {
let initial_content = "struct Book {\n string name;\n string writer;\n uint id;\n bool available;\n}\n".to_string();
let final_content =
"struct Book {\n string name;\n string id;\n bool available;\n}\n".to_string();
assert_eq!(
final_content,
update_file_contents(
initial_content,
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 2,
character: 11
},
end: Position {
line: 3,
character: 9
}
}),
range_length: Some(17),
text: String::new(),
}
),
);
}
#[test]
fn add_content() {
let initial_content =
"struct Book {\n string name;\n string id;\n bool available;\n}\n".to_string();
let final_content = "struct Book {\n string name;\n string writer;\n uint id;\n bool available;\n}\n".to_string();
assert_eq!(
final_content,
update_file_contents(
initial_content,
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 2,
character: 11
},
end: Position {
line: 2,
character: 11
}
}),
range_length: Some(0),
text: "writer;\n uint ".to_string(),
}
),
);
}
}