Skip to main content

lintspec_core/
utils.rs

1//! Utils for parsing Solidity source code.
2use std::{fmt::Write as _, path::Path, sync::LazyLock};
3
4use regex::Regex;
5pub use semver;
6use semver::{Version, VersionReq};
7use slang_solidity::{
8    cst::{NonterminalKind, Query, TextIndex},
9    parser::Parser,
10    utils::LanguageFacts,
11};
12
13use crate::{
14    error::{ErrorKind, Result},
15    prelude::OrPanic as _,
16};
17
18/// A regex to identify version pragma statements so that the whole file does not need to be parsed.
19static REGEX: LazyLock<Regex> = LazyLock::new(|| {
20    Regex::new(r"pragma\s+solidity[^;]+;").or_panic("the version pragma regex should compile")
21});
22
23/// Search for `pragma solidity` statements in the source and return the highest matching Solidity version.
24///
25/// If no pragma directive is found, the version defaults to `0.8.0`. Only the first pragma directive is considered,
26/// other ones in the file are ignored. Multiple version specifiers separated by a space are taken as meaning "and",
27/// specifiers separated by `||` are taken as meaning "or". Spaces take precedence over double-pipes.
28///
29/// Example: `0.6.0 || >=0.7.0 <0.8.0` means "either 0.6.0 or 0.7.x".
30///
31/// Within the specifiers' constraints, the highest version that is supported by [`slang_solidity`] is returned. In
32/// the above example, version `0.7.6` would be used.
33///
34/// # Errors
35/// This function errors if the found version string cannot be parsed to a [`VersionReq`] or if the version is not
36/// supported by [`slang_solidity`].
37///
38/// # Panics
39/// This function panics if the [`LanguageFacts::ALL_VERSIONS`] list is empty.
40///
41/// # Examples
42///
43/// ```
44/// # use std::path::PathBuf;
45/// # use lintspec_core::utils::{detect_solidity_version, semver::Version};
46/// assert_eq!(
47///     detect_solidity_version("pragma solidity >=0.8.4 <0.8.26;", PathBuf::from("./file.sol")).unwrap(),
48///     Version::new(0, 8, 25)
49/// );
50/// assert_eq!(
51///     detect_solidity_version("pragma solidity ^0.4.0 || 0.6.x;", PathBuf::from("./file.sol")).unwrap(),
52///     Version::new(0, 6, 12)
53/// );
54/// assert_eq!(
55///     detect_solidity_version("contract Foo {}", PathBuf::from("./file.sol")).unwrap(),
56///     Version::new(0, 8, 0)
57/// );
58/// // this version of Solidity does not exist
59/// assert!(detect_solidity_version("pragma solidity 0.7.7;", PathBuf::from("./file.sol")).is_err());
60/// ```
61pub fn detect_solidity_version(src: impl AsRef<str>, path: impl AsRef<Path>) -> Result<Version> {
62    fn inner(src: &str, path: &Path) -> Result<Version> {
63        let Some(pragma) = REGEX.find(src) else {
64            return Ok(Version::new(0, 8, 0));
65        };
66
67        let parser = Parser::create(get_latest_supported_version()).or_panic(
68            "the Parser should be initialized correctly with a supported solidity version",
69        );
70
71        let parse_result =
72            parser.parse_nonterminal(NonterminalKind::PragmaDirective, pragma.as_str());
73        if !parse_result.is_valid() {
74            let Some(error) = parse_result.errors().first() else {
75                return Err(ErrorKind::UnknownError.into());
76            };
77            return Err(ErrorKind::ParsingError {
78                path: path.to_path_buf(),
79                loc: error.text_range().start.into(),
80                message: error.message(),
81            }
82            .into());
83        }
84
85        let cursor = parse_result.create_tree_cursor();
86        let query_set = Query::create("@version_set [VersionExpressionSet]")
87            .or_panic("version set query should compile");
88        let query_expr = Query::create("@version_expr [VersionExpression]")
89            .or_panic("version expr query should compile");
90
91        let mut version_reqs = Vec::new();
92        for m in cursor.query(vec![query_set]) {
93            let Some(Some(set)) = m
94                .capture("version_set")
95                .map(|capture| capture.cursors().first().cloned())
96            else {
97                continue;
98            };
99            version_reqs.push(String::new());
100            let cursor = set.node().create_cursor(TextIndex::default());
101            for m in cursor.query(vec![query_expr.clone()]) {
102                let Some(Some(expr)) = m
103                    .capture("version_expr")
104                    .map(|capture| capture.cursors().first().cloned())
105                else {
106                    continue;
107                };
108                let text = expr.node().unparse();
109                let text = text.trim();
110                // check if we are dealing with a version range with hyphen format
111                let v = version_reqs.last_mut().ok_or(ErrorKind::ParsingError {
112                    path: path.to_path_buf(),
113                    loc: expr.text_range().start.into(),
114                    message: "version expression is not in an expression set".to_string(),
115                })?;
116                if let Some((start, end)) = text.split_once('-') {
117                    let _ = write!(v, ",>={},<={}", start.trim(), end.trim());
118                } else {
119                    // for `semver`, the different specifiers should be combined with a comma if they must all match
120                    if let Some(true) = text.chars().next().map(|c| c.is_ascii_digit()) {
121                        // for `semver`, no comparator is the same as the caret comparator, but for solidity it means `=`
122                        let _ = write!(v, ",={text}");
123                    } else {
124                        let _ = write!(v, ",{text}");
125                    }
126                }
127            }
128        }
129        let reqs = version_reqs
130            .into_iter()
131            .map(|r| VersionReq::parse(r.trim_start_matches(',')).map_err(Into::into))
132            .collect::<Result<Vec<_>>>()?;
133        reqs.iter()
134            .filter_map(|r| {
135                LanguageFacts::ALL_VERSIONS
136                    .iter()
137                    .rev()
138                    .find(|v| r.matches(v))
139            })
140            .max()
141            .cloned()
142            .ok_or_else(|| {
143                ErrorKind::SolidityUnsupportedVersion(pragma.as_str().to_string()).into()
144            })
145    }
146    inner(src.as_ref(), path.as_ref())
147}
148
149/// Get the latest Solidity version supported by the [`slang_solidity`] parser
150#[must_use]
151pub fn get_latest_supported_version() -> Version {
152    LanguageFacts::LATEST_VERSION
153}