ink_analyzer/
codegen.rs

1//! Utilities for generate ink! project files.
2
3pub mod snippets;
4
5use crate::codegen::snippets::{
6    CARGO_TOML_PLAIN, CARGO_TOML_PLAIN_V4, CARGO_TOML_PLAIN_V5, CARGO_TOML_SNIPPET,
7    CARGO_TOML_SNIPPET_V4, CARGO_TOML_SNIPPET_V5, CONTRACT_PLAIN, CONTRACT_PLAIN_V4,
8    CONTRACT_PLAIN_V5, CONTRACT_SNIPPET, CONTRACT_SNIPPET_V4, CONTRACT_SNIPPET_V5,
9};
10use crate::{utils, Version};
11
12/// Code stubs/snippets for creating an ink! project
13/// (i.e. code stubs/snippets for `lib.rs` and `Cargo.toml`).
14#[derive(Debug, PartialEq, Eq)]
15pub struct Project {
16    /// The `lib.rs` content.
17    pub lib: ProjectFile,
18    /// The `Cargo.toml` content.
19    pub cargo: ProjectFile,
20}
21
22/// Code stubs/snippets for creating a file in an ink! project
23/// (e.g. `lib.rs` or `Cargo.toml` for an ink! contract).
24#[derive(Debug, PartialEq, Eq)]
25pub struct ProjectFile {
26    /// A plain text code stub.
27    pub plain: String,
28    /// A snippet (i.e. with tab stops and/or placeholders).
29    pub snippet: Option<String>,
30}
31
32/// An ink! project error.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum Error {
36    /// Invalid package name.
37    ///
38    /// Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
39    PackageName,
40    /// Invalid contract name.
41    ///
42    /// Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
43    ContractName,
44}
45
46/// Returns code stubs/snippets for creating a new ink! project given a name.
47pub fn new_project(name: String, version: Version) -> Result<Project, Error> {
48    // Validates that name is a valid Rust package name.
49    // Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
50    if name.is_empty()
51        || !name
52            .chars()
53            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
54    {
55        return Err(Error::PackageName);
56    }
57
58    // Validates that name is a valid ink! contract name (i.e. contract names must additionally begin with an alphabetic character).
59    // Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
60    if !name.chars().next().is_some_and(char::is_alphabetic) {
61        return Err(Error::ContractName);
62    }
63
64    // Generates `mod` and storage `struct` names for the contract.
65    let module_name = name.replace('-', "_");
66    let struct_name = utils::pascal_case(&module_name);
67
68    // Returns project code stubs/snippets.
69    Ok(Project {
70        // Generates `lib.rs`.
71        lib: ProjectFile {
72            plain: if version.is_legacy() {
73                CONTRACT_PLAIN_V4
74            } else if version.is_v5() {
75                CONTRACT_PLAIN_V5
76            } else {
77                CONTRACT_PLAIN
78            }
79            .replace("my_contract", &module_name)
80            .replace("MyContract", &struct_name),
81            snippet: Some(
82                if version.is_legacy() {
83                    CONTRACT_SNIPPET_V4
84                } else if version.is_v5() {
85                    CONTRACT_SNIPPET_V5
86                } else {
87                    CONTRACT_SNIPPET
88                }
89                .replace("my_contract", &module_name)
90                .replace("MyContract", &struct_name),
91            ),
92        },
93        // Generates `Cargo.toml`.
94        cargo: ProjectFile {
95            plain: if version.is_legacy() {
96                CARGO_TOML_PLAIN_V4
97            } else if version.is_v5() {
98                CARGO_TOML_PLAIN_V5
99            } else {
100                CARGO_TOML_PLAIN
101            }
102            .replace("my_contract", &name),
103            snippet: Some(
104                if version.is_legacy() {
105                    CARGO_TOML_SNIPPET_V4
106                } else if version.is_v5() {
107                    CARGO_TOML_SNIPPET_V5
108                } else {
109                    CARGO_TOML_SNIPPET
110                }
111                .replace("my_contract", &name),
112            ),
113        },
114    })
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::{Analysis, MinorVersion};
121
122    // Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
123    // Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
124    #[test]
125    fn invalid_project_name_fails() {
126        for (name, expected_error) in [
127            // Empty.
128            ("", Error::PackageName),
129            // Disallowed characters (i.e. not alphanumeric, `-` or `_`).
130            ("hello!", Error::PackageName),
131            ("hello world", Error::PackageName),
132            ("💝", Error::PackageName),
133            // Starts with non-alphabetic character.
134            ("1hello", Error::ContractName),
135            ("-hello", Error::ContractName),
136            ("_hello", Error::ContractName),
137        ] {
138            assert_eq!(
139                new_project(name.to_owned(), Version::Legacy),
140                Err(expected_error)
141            );
142        }
143    }
144
145    #[test]
146    fn valid_project_name_works() {
147        for name in ["hello", "hello_world", "hello-world"] {
148            // Generates an ink! contract project.
149            let result = new_project(name.to_owned(), Version::Legacy);
150            assert!(result.is_ok());
151
152            // Verifies that the generated code stub is a valid contract.
153            let contract_code = result.unwrap().lib.plain;
154            let analysis = Analysis::new(&contract_code, Version::Legacy);
155            assert_eq!(analysis.diagnostics().len(), 0);
156        }
157    }
158
159    #[test]
160    fn new_project_works() {
161        for version in [
162            Version::Legacy,
163            Version::V5(MinorVersion::Latest),
164            Version::V6,
165        ] {
166            // Generates an ink! contract project.
167            let result = new_project("hello_world".to_owned(), version);
168            assert!(result.is_ok());
169
170            // Verifies the generated code stub and `Cargo.toml` file.
171            let project = result.unwrap();
172            let cargo_toml = project.cargo.plain;
173            assert!(cargo_toml.contains(if version.is_legacy() {
174                r#"ink = { version = "4"#
175            } else if version.is_v5() {
176                r#"ink = { version = "5"#
177            } else {
178                r#"version = "6"#
179            }));
180            let contract_code = project.lib.plain;
181            let analysis = Analysis::new(&contract_code, version);
182            assert_eq!(analysis.diagnostics().len(), 0);
183        }
184    }
185}