Skip to main content

solar_config/
opts.rs

1//! Solar CLI arguments.
2
3use crate::{
4    ColorChoice, CompilerOutput, CompilerStage, Dump, ErrorFormat, EvmVersion, HumanEmitterKind,
5    ImportRemapping, Language, OptimizationMode, Threads,
6};
7use std::{num::NonZeroUsize, path::PathBuf};
8
9#[cfg(feature = "clap")]
10use clap::{Parser, ValueHint};
11
12// TODO: implement `allow_paths`.
13
14/// Compilation configuration.
15#[derive(Clone, Debug, Default)]
16#[cfg_attr(feature = "clap", derive(Parser))]
17#[cfg_attr(feature = "clap", command(
18    name = "solar",
19    version = crate::version::short_version(),
20    long_version = crate::version::version(),
21    arg_required_else_help = true,
22))]
23#[allow(clippy::manual_non_exhaustive)]
24pub struct CompileOpts {
25    /// Files to compile, or import remappings.
26    ///
27    /// `-` specifies standard input.
28    ///
29    /// In Standard JSON mode, no input or `-` reads from standard input; otherwise, exactly one
30    /// input file may be specified.
31    ///
32    /// Import remappings are specified as `[context:]prefix=path`.
33    /// See <https://docs.soliditylang.org/en/latest/path-resolution.html#import-remapping>.
34    // NOTE: Remappings are parsed away into the `import_remappings` field. Use that instead.
35    #[cfg_attr(feature = "clap", arg(value_hint = ValueHint::FilePath))]
36    pub input: Vec<String>,
37    /// Import remappings.
38    ///
39    /// This is either added manually when constructing the session or parsed from `input` into
40    /// this field.
41    ///
42    /// See <https://docs.soliditylang.org/en/latest/path-resolution.html#import-remapping>.
43    #[cfg_attr(feature = "clap", arg(skip))]
44    pub import_remappings: Vec<ImportRemapping>,
45    /// Use the given path as the root of the source tree.
46    #[cfg_attr(
47        feature = "clap",
48        arg(
49            help_heading = "Input options",
50            long,
51            value_hint = ValueHint::DirPath,
52        )
53    )]
54    pub base_path: Option<PathBuf>,
55    /// Directory to search for files.
56    ///
57    /// Can be used multiple times.
58    #[cfg_attr(
59        feature = "clap",
60        arg(
61            help_heading = "Input options",
62            name = "include-path",
63            value_name = "INCLUDE_PATH",
64            long,
65            short = 'I',
66            alias = "import-path",
67            value_hint = ValueHint::DirPath,
68        )
69    )]
70    pub include_paths: Vec<PathBuf>,
71    /// Allow a given path for imports.
72    #[cfg_attr(
73        feature = "clap",
74        arg(
75            help_heading = "Input options",
76            long,
77            value_delimiter = ',',
78            value_hint = ValueHint::DirPath,
79        )
80    )]
81    pub allow_paths: Vec<PathBuf>,
82    /// Source code language. Only Solidity is currently implemented.
83    #[cfg_attr(
84        feature = "clap",
85        arg(help_heading = "Input options", long, value_enum, default_value_t, hide = true)
86    )]
87    pub language: Language,
88
89    /// Number of threads to use. Zero specifies the number of logical cores.
90    #[cfg_attr(feature = "clap", arg(long, short = 'j', visible_alias = "jobs", default_value_t))]
91    pub threads: Threads,
92    /// EVM version.
93    #[cfg_attr(feature = "clap", arg(long, value_enum, default_value_t))]
94    pub evm_version: EvmVersion,
95    /// Stop execution after the given compiler stage.
96    #[cfg_attr(feature = "clap", arg(long, value_enum))]
97    pub stop_after: Option<CompilerStage>,
98    /// MIR optimization objective.
99    #[cfg_attr(feature = "clap", arg(short = 'O', long = "optimize", value_enum, default_value_t))]
100    pub optimization: OptimizationMode,
101
102    /// Directory to write output files.
103    #[cfg_attr(feature = "clap", arg(long, value_hint = ValueHint::DirPath))]
104    pub out_dir: Option<PathBuf>,
105    /// Comma separated list of types of output for the compiler to emit.
106    #[cfg_attr(feature = "clap", arg(long, value_delimiter = ','))]
107    pub emit: Vec<CompilerOutput>,
108
109    /// Switch to Standard JSON input/output mode.
110    #[cfg_attr(feature = "clap", arg(long))]
111    pub standard_json: bool,
112
113    /// Coloring.
114    #[cfg_attr(
115        feature = "clap",
116        arg(help_heading = "Display options", long, value_parser = ColorChoiceValueParser::default(), default_value = "auto")
117    )]
118    pub color: ColorChoice,
119    /// Use verbose output.
120    #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long, short))]
121    pub verbose: bool,
122    /// Pretty-print JSON output.
123    ///
124    /// Does not include errors. See `--pretty-json-err`.
125    #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long))]
126    pub pretty_json: bool,
127    /// Pretty-print error JSON output.
128    #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long))]
129    pub pretty_json_err: bool,
130    /// How errors and other messages are produced.
131    #[cfg_attr(
132        feature = "clap",
133        arg(help_heading = "Display options", long, value_enum, default_value_t)
134    )]
135    pub error_format: ErrorFormat,
136    /// Human-readable error message style.
137    #[cfg_attr(
138        feature = "clap",
139        arg(
140            help_heading = "Display options",
141            long,
142            value_name = "VALUE",
143            value_enum,
144            default_value_t
145        )
146    )]
147    pub error_format_human: HumanEmitterKind,
148    /// Terminal width for error message formatting.
149    #[cfg_attr(
150        feature = "clap",
151        arg(help_heading = "Display options", long, value_name = "WIDTH")
152    )]
153    pub diagnostic_width: Option<usize>,
154    /// Whether to disable warnings.
155    #[cfg_attr(feature = "clap", arg(help_heading = "Display options", long))]
156    pub no_warnings: bool,
157    /// Comma separated list of diagnostic codes to allow.
158    #[cfg_attr(
159        feature = "clap",
160        arg(help_heading = "Display options", long, value_name = "CODE", value_delimiter = ',')
161    )]
162    pub allow: Vec<String>,
163
164    /// Unstable flags. WARNING: these are completely unstable, and may change at any time.
165    ///
166    /// See `-Zhelp` for more details.
167    #[doc(hidden)]
168    #[cfg_attr(feature = "clap", arg(id = "unstable-features", value_name = "FLAG", short = 'Z'))]
169    pub _unstable: Vec<String>,
170
171    /// Parsed unstable flags.
172    #[cfg_attr(feature = "clap", arg(skip))]
173    pub unstable: UnstableOpts,
174
175    // Allows `CompileOpts { x: y, ..Default::default() }`.
176    #[doc(hidden)]
177    #[cfg_attr(feature = "clap", arg(skip))]
178    pub _non_exhaustive: (),
179}
180
181impl CompileOpts {
182    /// Returns whether MIR optimization passes should run during codegen.
183    #[inline]
184    pub const fn optimize_mir(&self) -> bool {
185        !matches!(self.optimization, OptimizationMode::None)
186    }
187
188    /// Returns the number of threads to use.
189    #[inline]
190    pub fn threads(&self) -> NonZeroUsize {
191        self.threads.0
192    }
193
194    /// Finishes argument parsing.
195    #[cfg(feature = "clap")]
196    pub fn finish(&mut self) -> Result<(), clap::Error> {
197        if self.standard_json {
198            if self.input.iter().any(|s| s.contains('=')) {
199                return Err(make_clap_error(
200                    clap::error::ErrorKind::InvalidValue,
201                    "Import remappings are not accepted on the command line in Standard JSON mode.\n\
202                     Please put them under 'settings.remappings' in the JSON input.",
203                ));
204            }
205            if self.input.len() > 1 {
206                return Err(make_clap_error(
207                    clap::error::ErrorKind::TooManyValues,
208                    "Too many input files for --standard-json.\n\
209                     Please either specify a single file name or provide its content on standard input.",
210                ));
211            }
212        }
213
214        self.import_remappings = self
215            .input
216            .iter()
217            .filter(|s| s.contains('='))
218            .map(|s| {
219                s.parse::<ImportRemapping>().map_err(|e| {
220                    make_clap_error(
221                        clap::error::ErrorKind::InvalidValue,
222                        format!("invalid remapping {s:?}: {e}"),
223                    )
224                })
225            })
226            .collect::<Result<_, _>>()?;
227        self.input.retain(|s| !s.contains('='));
228
229        if !self._unstable.is_empty() {
230            let hack = self._unstable.iter().map(|s| format!("--{s}"));
231            let args = std::iter::once(String::new()).chain(hack);
232            self.unstable = UnstableOpts::try_parse_from(args).map_err(|e| {
233                override_clap_message(e, |s| {
234                    s.replace("solar-config", "solar").replace("error:", "").replace("--", "-Z")
235                })
236            })?;
237        }
238
239        Ok(())
240    }
241}
242
243// Ideally would be clap::Error::raw but it never prints styled text.
244#[cfg(feature = "clap")]
245fn override_clap_message(e: clap::Error, f: impl FnOnce(String) -> String) -> clap::Error {
246    let msg = f(e.render().ansi().to_string());
247    let msg = msg.trim();
248    make_clap_error(e.kind(), msg)
249}
250
251#[cfg(feature = "clap")]
252fn make_clap_error(kind: clap::error::ErrorKind, message: impl std::fmt::Display) -> clap::Error {
253    <CompileOpts as clap::CommandFactory>::command().error(kind, message)
254}
255
256#[cfg(feature = "clap")]
257#[derive(Clone, Default)]
258struct ColorChoiceValueParser(clap::builder::EnumValueParser<clap::ColorChoice>);
259
260#[cfg(feature = "clap")]
261impl clap::builder::TypedValueParser for ColorChoiceValueParser {
262    type Value = ColorChoice;
263
264    fn parse_ref(
265        &self,
266        cmd: &clap::Command,
267        arg: Option<&clap::Arg>,
268        value: &std::ffi::OsStr,
269    ) -> Result<Self::Value, clap::Error> {
270        self.0.parse_ref(cmd, arg, value).map(map_color_choice)
271    }
272
273    fn possible_values(
274        &self,
275    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
276        self.0.possible_values()
277    }
278}
279
280#[cfg(feature = "clap")]
281fn map_color_choice(c: clap::ColorChoice) -> ColorChoice {
282    match c {
283        clap::ColorChoice::Auto => ColorChoice::Auto,
284        clap::ColorChoice::Always => ColorChoice::Always,
285        clap::ColorChoice::Never => ColorChoice::Never,
286    }
287}
288
289/// Internal options.
290#[derive(Clone, Debug, Default)]
291#[cfg_attr(feature = "clap", derive(Parser))]
292#[cfg_attr(feature = "clap", clap(
293    disable_help_flag = true,
294    before_help = concat!(
295        "List of all unstable flags.\n",
296        "WARNING: these are completely unstable, and may change at any time!",
297    ),
298    help_template = "{before-help}{all-args}"
299))]
300#[allow(clippy::manual_non_exhaustive)]
301pub struct UnstableOpts {
302    /// Enables UI testing mode.
303    #[cfg_attr(feature = "clap", arg(long))]
304    pub ui_testing: bool,
305
306    /// Prints a note for every diagnostic that is emitted with the creation and emission location.
307    ///
308    /// This is enabled by default on debug builds.
309    #[cfg_attr(feature = "clap", arg(long))]
310    pub track_diagnostics: bool,
311
312    /// Enables parsing Yul files for testing.
313    #[cfg_attr(feature = "clap", arg(long))]
314    pub parse_yul: bool,
315
316    /// Disables import resolution.
317    #[cfg_attr(feature = "clap", arg(long))]
318    pub no_resolve_imports: bool,
319
320    /// Print additional information about the compiler's internal state.
321    ///
322    /// Valid kinds are `ast` and `hir`.
323    #[cfg_attr(feature = "clap", arg(long, require_equals = true, value_name = "KIND[=PATHS...]"))]
324    pub dump: Option<Dump>,
325
326    /// Print AST stats.
327    #[cfg_attr(feature = "clap", arg(long))]
328    pub ast_stats: bool,
329
330    /// Print HIR stats.
331    #[cfg_attr(feature = "clap", arg(long))]
332    pub hir_stats: bool,
333
334    /// Print Standard JSON input stats.
335    #[cfg_attr(feature = "clap", arg(long))]
336    pub standard_json_stats: bool,
337
338    /// Run the span visitor after parsing.
339    #[cfg_attr(feature = "clap", arg(long))]
340    pub span_visitor: bool,
341
342    /// Print contracts' max storage sizes.
343    #[cfg_attr(feature = "clap", arg(long))]
344    pub print_max_storage_sizes: bool,
345
346    /// Print resolved NatSpec docs as diagnostics for UI tests.
347    #[cfg_attr(feature = "clap", arg(long))]
348    pub print_natspec: bool,
349
350    /// Type check the program. WIP.
351    #[cfg_attr(feature = "clap", arg(long))]
352    pub typeck: bool,
353
354    /// Print MIR after every MIR optimization pass during codegen.
355    #[cfg_attr(feature = "clap", arg(long))]
356    pub mir_print_after_each: bool,
357
358    /// Enable the experimental EVM code generator (MIR lowering and backend).
359    ///
360    /// Off by default: MIR and bytecode output is only produced when this is
361    /// set. Codegen is a work in progress and not yet part of the compiler's
362    /// stable, solc-compatible behavior.
363    #[cfg_attr(feature = "clap", arg(long))]
364    pub codegen: bool,
365
366    // ----------------------------------------
367    // Please add new options above this point!
368    // ----------------------------------------
369    /// Print help.
370    #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Help))]
371    pub help: (),
372
373    // Allows `UnstableOpts { x: y, ..Default::default() }`.
374    #[doc(hidden)]
375    #[cfg_attr(feature = "clap", arg(skip))]
376    pub _non_exhaustive: (),
377
378    #[cfg(test)]
379    #[cfg_attr(feature = "clap", arg(long))]
380    pub test_bool: bool,
381
382    #[cfg(test)]
383    #[cfg_attr(feature = "clap", arg(long))]
384    pub test_value: Option<usize>,
385}
386
387#[cfg(all(test, feature = "clap"))]
388mod tests {
389    use super::*;
390    use clap::CommandFactory;
391
392    #[test]
393    fn verify_cli() {
394        CompileOpts::command().debug_assert();
395        let _ = CompileOpts::default();
396        let _ = CompileOpts { evm_version: EvmVersion::Berlin, ..Default::default() };
397
398        UnstableOpts::command().debug_assert();
399        let _ = UnstableOpts::default();
400        let _ = UnstableOpts { ast_stats: false, ..Default::default() };
401    }
402
403    #[test]
404    fn allow() {
405        let mut opts =
406            CompileOpts::try_parse_from(["solar", "--allow", "1234,5678", "a.sol"]).unwrap();
407        opts.finish().unwrap();
408
409        assert_eq!(opts.allow, ["1234", "5678"]);
410    }
411
412    #[test]
413    fn standard_json_input() {
414        let mut opts = CompileOpts::try_parse_from(["solar", "--standard-json"]).unwrap();
415        opts.finish().unwrap();
416        assert!(opts.input.is_empty());
417
418        let mut opts = CompileOpts::try_parse_from(["solar", "--standard-json", "-"]).unwrap();
419        opts.finish().unwrap();
420        assert_eq!(opts.input, ["-"]);
421
422        let mut opts =
423            CompileOpts::try_parse_from(["solar", "--standard-json", "input.json"]).unwrap();
424        opts.finish().unwrap();
425        assert_eq!(opts.input, ["input.json"]);
426    }
427
428    #[test]
429    fn standard_json_rejects_multiple_inputs() {
430        let mut opts =
431            CompileOpts::try_parse_from(["solar", "--standard-json", "input1.json", "input2.json"])
432                .unwrap();
433        let error = opts.finish().unwrap_err().render().ansi().to_string();
434        assert!(error.contains("Too many input files for --standard-json."));
435    }
436
437    #[test]
438    fn standard_json_rejects_remappings() {
439        let mut opts = CompileOpts::try_parse_from(["solar", "--standard-json", "a=b"]).unwrap();
440        let error = opts.finish().unwrap_err().render().ansi().to_string();
441        assert!(error.contains("Import remappings are not accepted on the command line"));
442    }
443
444    #[test]
445    fn unstable_features() {
446        fn parse(args: &[&str]) -> Result<UnstableOpts, impl std::fmt::Debug> {
447            struct UnwrapDisplay<T>(T);
448            impl<T: std::fmt::Display> std::fmt::Debug for UnwrapDisplay<T> {
449                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450                    write!(f, "\n{}", self.0)
451                }
452            }
453            (|| {
454                let mut opts = CompileOpts::try_parse_from(args)?;
455                opts.finish()?;
456                Ok::<_, clap::Error>(opts.unstable)
457            })()
458            .map_err(|e| UnwrapDisplay(e.render().ansi().to_string()))
459        }
460
461        let unstable = parse(&["solar", "a.sol"]).unwrap();
462        assert!(!unstable.test_bool);
463
464        let unstable = parse(&["solar", "-Ztest-bool", "a.sol"]).unwrap();
465        assert!(unstable.test_bool);
466        let unstable = parse(&["solar", "-Z", "test-bool", "a.sol"]).unwrap();
467        assert!(unstable.test_bool);
468
469        assert!(parse(&["solar", "-Ztest-value", "a.sol"]).is_err());
470        assert!(parse(&["solar", "-Z", "test-value", "a.sol"]).is_err());
471        assert!(parse(&["solar", "-Ztest-value", "2", "a.sol"]).is_err());
472        let unstable = parse(&["solar", "-Ztest-value=2", "a.sol"]).unwrap();
473        assert_eq!(unstable.test_value, Some(2));
474        let unstable = parse(&["solar", "-Z", "test-value=2", "a.sol"]).unwrap();
475        assert_eq!(unstable.test_value, Some(2));
476
477        // DON'T ADD ANY MORE TESTS HERE.
478    }
479}