use std::{fmt::Write as _, path::Path, sync::LazyLock};
use regex::Regex;
pub use semver;
use semver::{Version, VersionReq};
use slang_solidity::{
cst::{NonterminalKind, Query, TextIndex},
parser::Parser,
utils::LanguageFacts,
};
use crate::{
error::{Error, Result},
prelude::OrPanic as _,
};
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"pragma\s+solidity[^;]+;").or_panic("the version pragma regex should compile")
});
pub fn detect_solidity_version(src: impl AsRef<str>, path: impl AsRef<Path>) -> Result<Version> {
fn inner(src: &str, path: &Path) -> Result<Version> {
let Some(pragma) = REGEX.find(src) else {
return Ok(Version::new(0, 8, 0));
};
let parser = Parser::create(get_latest_supported_version()).or_panic(
"the Parser should be initialized correctly with a supported solidity version",
);
let parse_result =
parser.parse_nonterminal(NonterminalKind::PragmaDirective, pragma.as_str());
if !parse_result.is_valid() {
let Some(error) = parse_result.errors().first() else {
return Err(Error::UnknownError);
};
return Err(Error::ParsingError {
path: path.to_path_buf(),
loc: error.text_range().start.into(),
message: error.message(),
});
}
let cursor = parse_result.create_tree_cursor();
let query_set = Query::create("@version_set [VersionExpressionSet]")
.or_panic("version set query should compile");
let query_expr = Query::create("@version_expr [VersionExpression]")
.or_panic("version expr query should compile");
let mut version_reqs = Vec::new();
for m in cursor.query(vec![query_set]) {
let Some(Some(set)) = m
.capture("version_set")
.map(|capture| capture.cursors().first().cloned())
else {
continue;
};
version_reqs.push(String::new());
let cursor = set.node().create_cursor(TextIndex::default());
for m in cursor.query(vec![query_expr.clone()]) {
let Some(Some(expr)) = m
.capture("version_expr")
.map(|capture| capture.cursors().first().cloned())
else {
continue;
};
let text = expr.node().unparse();
let text = text.trim();
let v = version_reqs.last_mut().ok_or(Error::ParsingError {
path: path.to_path_buf(),
loc: expr.text_range().start.into(),
message: "version expression is not in an expression set".to_string(),
})?;
if let Some((start, end)) = text.split_once('-') {
let _ = write!(v, ",>={},<={}", start.trim(), end.trim());
} else {
if let Some(true) = text.chars().next().map(|c| c.is_ascii_digit()) {
let _ = write!(v, ",={text}");
} else {
let _ = write!(v, ",{text}");
}
}
}
}
let reqs = version_reqs
.into_iter()
.map(|r| VersionReq::parse(r.trim_start_matches(',')).map_err(Into::into))
.collect::<Result<Vec<_>>>()?;
reqs.iter()
.filter_map(|r| {
LanguageFacts::ALL_VERSIONS
.iter()
.rev()
.find(|v| r.matches(v))
})
.max()
.cloned()
.ok_or_else(|| Error::SolidityUnsupportedVersion(pragma.as_str().to_string()))
}
inner(src.as_ref(), path.as_ref())
}
#[must_use]
pub fn get_latest_supported_version() -> Version {
LanguageFacts::LATEST_VERSION
}