foundry_compilers_core/utils/
re.rs

1use regex::{Match, Regex};
2use std::sync::LazyLock as Lazy;
3
4/// A regex that matches the import path and identifier of a solidity import
5/// statement with the named groups "path", "id".
6// Adapted from <https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100>
7pub static RE_SOL_IMPORT: Lazy<Regex> = Lazy::new(|| {
8    Regex::new(r#"import\s+(?:(?:"(?P<p1>.*)"|'(?P<p2>.*)')(?:\s+as\s+\w+)?|(?:(?:\w+(?:\s+as\s+\w+)?|\*\s+as\s+\w+|\{\s*(?:\w+(?:\s+as\s+\w+)?(?:\s*,\s*)?)+\s*\})\s+from\s+(?:"(?P<p3>.*)"|'(?P<p4>.*)')))\s*;"#).unwrap()
9});
10
11/// A regex that matches an alias within an import statement
12pub static RE_SOL_IMPORT_ALIAS: Lazy<Regex> =
13    Lazy::new(|| Regex::new(r#"(?:(?P<target>\w+)|\*|'|")\s+as\s+(?P<alias>\w+)"#).unwrap());
14
15/// A regex that matches the version part of a solidity pragma
16/// as follows: `pragma solidity ^0.5.2;` => `^0.5.2`
17/// statement with the named group "version".
18// Adapted from <https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L119>
19pub static RE_SOL_PRAGMA_VERSION: Lazy<Regex> =
20    Lazy::new(|| Regex::new(r"pragma\s+solidity\s+(?P<version>.+?);").unwrap());
21
22/// A regex that matches the SDPX license identifier
23/// statement with the named group "license".
24pub static RE_SOL_SDPX_LICENSE_IDENTIFIER: Lazy<Regex> =
25    Lazy::new(|| Regex::new(r"///?\s*SPDX-License-Identifier:\s*(?P<license>.+)").unwrap());
26
27/// A regex used to remove extra lines in flatenned files
28pub static RE_THREE_OR_MORE_NEWLINES: Lazy<Regex> = Lazy::new(|| Regex::new("\n{3,}").unwrap());
29
30/// A regex used to remove extra lines in flatenned files
31pub static RE_TWO_OR_MORE_SPACES: Lazy<Regex> = Lazy::new(|| Regex::new(" {2,}").unwrap());
32
33/// A regex that matches version pragma in a Vyper
34pub static RE_VYPER_VERSION: Lazy<Regex> =
35    Lazy::new(|| Regex::new(r"#(?:pragma version|@version)\s+(?P<version>.+)").unwrap());
36
37/// A regex that matches the contract names in a Solidity file.
38pub static RE_CONTRACT_NAMES: Lazy<Regex> = Lazy::new(|| {
39    Regex::new(r"\b(?:contract|library|abstract\s+contract|interface)\s+([\w$]+)").unwrap()
40});
41
42/// Create a regex that matches any library or contract name inside a file
43pub fn create_contract_or_lib_name_regex(name: &str) -> Regex {
44    Regex::new(&format!(r#"(?:using\s+(?P<n1>{name})\s+|is\s+(?:\w+\s*,\s*)*(?P<n2>{name})(?:\s*,\s*\w+)*|(?:(?P<ignore>(?:function|error|as)\s+|\n[^\n]*(?:"([^"\n]|\\")*|'([^'\n]|\\')*))|\W+)(?P<n3>{name})(?:\.|\(| ))"#)).unwrap()
45}
46
47/// Returns all path parts from any solidity import statement in a string,
48/// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`.
49///
50/// See also <https://docs.soliditylang.org/en/v0.8.9/grammar.html>
51pub fn find_import_paths(contract: &str) -> impl Iterator<Item = Match<'_>> {
52    RE_SOL_IMPORT.captures_iter(contract).filter_map(|cap| {
53        cap.name("p1")
54            .or_else(|| cap.name("p2"))
55            .or_else(|| cap.name("p3"))
56            .or_else(|| cap.name("p4"))
57    })
58}
59
60/// Returns the solidity version pragma from the given input:
61/// `pragma solidity ^0.5.2;` => `^0.5.2`
62pub fn find_version_pragma(contract: &str) -> Option<Match<'_>> {
63    RE_SOL_PRAGMA_VERSION.captures(contract)?.name("version")
64}
65
66/// Given the regex and the target string, find all occurrences of named groups within the string.
67///
68/// This method returns the tuple of matches `(a, b)` where `a` is the match for the entire regex
69/// and `b` is the match for the first named group.
70///
71/// NOTE: This method will return the match for the first named group, so the order of passed named
72/// groups matters.
73pub fn capture_outer_and_inner<'a>(
74    content: &'a str,
75    regex: &regex::Regex,
76    names: &[&str],
77) -> Vec<(regex::Match<'a>, regex::Match<'a>)> {
78    regex
79        .captures_iter(content)
80        .filter_map(|cap| {
81            let cap_match = names.iter().find_map(|name| cap.name(name));
82            cap_match.and_then(|m| cap.get(0).map(|outer| (outer.to_owned(), m)))
83        })
84        .collect()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn can_find_import_paths() {
93        let s = r#"//SPDX-License-Identifier: Unlicense
94pragma solidity ^0.8.0;
95import "hardhat/console.sol";
96import "../contract/Contract.sol";
97import { T } from "../Test.sol";
98import { T } from '../Test2.sol';
99"#;
100        assert_eq!(
101            vec!["hardhat/console.sol", "../contract/Contract.sol", "../Test.sol", "../Test2.sol"],
102            find_import_paths(s).map(|m| m.as_str()).collect::<Vec<&str>>()
103        );
104    }
105
106    #[test]
107    fn can_find_version() {
108        let s = r"//SPDX-License-Identifier: Unlicense
109pragma solidity ^0.8.0;
110";
111        assert_eq!(Some("^0.8.0"), find_version_pragma(s).map(|s| s.as_str()));
112    }
113
114    #[test]
115    fn can_parse_curly_bracket_imports() {
116        let s =
117            r#"import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";"#;
118        let imports: Vec<_> = find_import_paths(s).map(|m| m.as_str()).collect();
119        assert_eq!(imports, vec!["@openzeppelin/contracts/utils/ReentrancyGuard.sol"])
120    }
121
122    #[test]
123    fn can_find_single_quote_imports() {
124        let content = r"
125// SPDX-License-Identifier: MIT
126pragma solidity 0.8.6;
127
128import '@openzeppelin/contracts/access/Ownable.sol';
129import '@openzeppelin/contracts/utils/Address.sol';
130
131import './../interfaces/IJBDirectory.sol';
132import './../libraries/JBTokens.sol';
133        ";
134        let imports: Vec<_> = find_import_paths(content).map(|m| m.as_str()).collect();
135
136        assert_eq!(
137            imports,
138            vec![
139                "@openzeppelin/contracts/access/Ownable.sol",
140                "@openzeppelin/contracts/utils/Address.sol",
141                "./../interfaces/IJBDirectory.sol",
142                "./../libraries/JBTokens.sol",
143            ]
144        );
145    }
146}