diff --git a/Cargo.lock b/Cargo.lock index 03b3abddec857..1427d4d05dd2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,16 +1168,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "ariadne" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f" -dependencies = [ - "unicode-width 0.1.14", - "yansi", -] - [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -2627,6 +2617,7 @@ dependencies = [ "clap", "dirs", "eyre", + "forge-doc", "forge-fmt", "foundry-cli", "foundry-common", @@ -4047,7 +4038,6 @@ dependencies = [ "foundry-evm", "foundry-evm-core", "foundry-linking", - "foundry-solang-parser", "foundry-test-utils", "foundry-wallets", "futures", @@ -4105,6 +4095,7 @@ dependencies = [ "regex", "serde", "serde_json", + "solar-interface", "thiserror 2.0.16", "toml 0.9.5", "tracing", @@ -4113,22 +4104,6 @@ dependencies = [ [[package]] name = "forge-fmt" version = "1.3.4" -dependencies = [ - "alloy-primitives", - "ariadne", - "foundry-config", - "foundry-solang-parser", - "itertools 0.14.0", - "similar-asserts", - "thiserror 2.0.16", - "toml 0.9.5", - "tracing", - "tracing-subscriber 0.3.20", -] - -[[package]] -name = "forge-fmt-2" -version = "1.3.4" dependencies = [ "foundry-common", "foundry-config", diff --git a/Cargo.toml b/Cargo.toml index 1e8611055f113..24625d2e6ca16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ members = [ "crates/evm/fuzz/", "crates/evm/traces/", "crates/fmt/", - "crates/fmt-2/", "crates/forge/", "crates/script-sequence/", "crates/macros/", diff --git a/crates/chisel/Cargo.toml b/crates/chisel/Cargo.toml index 9e7bdf363adc8..5a3ff68a0c35c 100644 --- a/crates/chisel/Cargo.toml +++ b/crates/chisel/Cargo.toml @@ -19,6 +19,7 @@ path = "bin/main.rs" [dependencies] # forge +forge-doc.workspace = true forge-fmt.workspace = true foundry-cli.workspace = true foundry-common.workspace = true diff --git a/crates/chisel/src/dispatcher.rs b/crates/chisel/src/dispatcher.rs index 6e5feca39fd78..5bfe5581093f9 100644 --- a/crates/chisel/src/dispatcher.rs +++ b/crates/chisel/src/dispatcher.rs @@ -9,8 +9,7 @@ use crate::{ }; use alloy_primitives::{Address, hex}; use eyre::{Context, Result}; -use forge_fmt::FormatterConfig; -use foundry_config::RpcEndpointUrl; +use foundry_config::{FormatterConfig, RpcEndpointUrl}; use foundry_evm::{ decode::decode_console_logs, traces::{ @@ -66,18 +65,8 @@ pub struct EtherscanABIResponse { /// Helper function that formats solidity source with the given [FormatterConfig] pub fn format_source(source: &str, config: FormatterConfig) -> eyre::Result { - match forge_fmt::parse(source) { - Ok(parsed) => { - let mut formatted_source = String::default(); - - if forge_fmt::format_to(&mut formatted_source, parsed, config).is_err() { - eyre::bail!("Could not format source!"); - } - - Ok(formatted_source) - } - Err(_) => eyre::bail!("Formatter could not parse source!"), - } + let formatted = forge_fmt::format(source, config).into_result()?; + Ok(formatted) } impl ChiselDispatcher { diff --git a/crates/chisel/src/source.rs b/crates/chisel/src/source.rs index aaf5c3788a021..e7709c2ac23f6 100644 --- a/crates/chisel/src/source.rs +++ b/crates/chisel/src/source.rs @@ -5,7 +5,7 @@ //! execution helpers. use eyre::Result; -use forge_fmt::solang_ext::{CodeLocationExt, SafeUnwrap}; +use forge_doc::solang_ext::{CodeLocationExt, SafeUnwrap}; use foundry_common::fs; use foundry_compilers::{ Artifact, ProjectCompileOutput, diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 21ff2e648563a..7adbe23ee4e82 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -2,6 +2,7 @@ use alloy_json_abi::JsonAbi; use alloy_primitives::{U256, map::HashMap}; use alloy_provider::{Provider, network::AnyNetwork}; use eyre::{ContextCompat, Result}; +use forge_fmt::FormatterConfig; use foundry_common::{ provider::{ProviderBuilder, RetryProvider}, shell, @@ -100,7 +101,7 @@ fn env_filter() -> tracing_subscriber::EnvFilter { pub fn abi_to_solidity(abi: &JsonAbi, name: &str) -> Result { let s = abi.to_sol(name, None); - let s = forge_fmt::format(&s)?; + let s = forge_fmt::format(&s, FormatterConfig::default()).into_result()?; Ok(s) } diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 95e26df179cc4..e9ca7ec0d7411 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -19,6 +19,7 @@ foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true +solar-interface.workspace = true alloy-primitives.workspace = true derive_more.workspace = true diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index a5ea23f349863..3dfa41428eec6 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -1,12 +1,11 @@ use crate::{ AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor, - document::DocumentContent, helpers::merge_toml_table, + document::DocumentContent, helpers::merge_toml_table, solang_ext::Visitable, }; use alloy_primitives::map::HashMap; use eyre::{Context, Result}; -use forge_fmt::{FormatterConfig, Visitable}; use foundry_compilers::{compilers::solc::SOLC_EXTENSIONS, utils::source_files_iter}; -use foundry_config::{DocConfig, filter::expand_globs}; +use foundry_config::{DocConfig, FormatterConfig, filter::expand_globs}; use itertools::Itertools; use mdbook::MDBook; use rayon::prelude::*; diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index c5201e73efb0d..65a90ddc6c70d 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -31,3 +31,6 @@ mod writer; pub use writer::{AsDoc, AsDocResult, BufWriter, Markdown}; pub use mdbook; + +// old formatter dependencies +pub mod solang_ext; diff --git a/crates/doc/src/parser/error.rs b/crates/doc/src/parser/error.rs index 770137f99d173..7a5cd818ba5a8 100644 --- a/crates/doc/src/parser/error.rs +++ b/crates/doc/src/parser/error.rs @@ -1,4 +1,4 @@ -use forge_fmt::FormatterError; +use solar_interface::diagnostics::EmittedDiagnostics; use thiserror::Error; /// The parser error. @@ -7,7 +7,7 @@ use thiserror::Error; pub enum ParserError { /// Formatter error. #[error(transparent)] - Formatter(#[from] FormatterError), + Formatter(EmittedDiagnostics), /// Internal parser error. #[error(transparent)] Internal(#[from] eyre::Error), diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index 18fcd085eb45c..6c5e6db22985c 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -1,8 +1,9 @@ -use crate::{Comments, error::ParserResult}; -use forge_fmt::{ - Comments as FmtComments, Formatter, FormatterConfig, InlineConfig, Visitor, +use crate::{ + Comments, + error::{ParserError, ParserResult}, solang_ext::SafeUnwrap, }; +use foundry_config::FormatterConfig; use solang_parser::pt::{ ContractDefinition, ContractTy, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, StructDefinition, TypeDefinition, VariableDefinition, @@ -82,33 +83,9 @@ impl ParseItem { /// Set formatted code on the [ParseItem]. pub fn with_code(mut self, source: &str, config: FormatterConfig) -> ParserResult { - let mut code = String::new(); - let mut fmt = Formatter::new( - &mut code, - source, - FmtComments::default(), - InlineConfig::default(), - config, - ); - - match self.source.clone() { - ParseSource::Contract(mut contract) => { - contract.parts = vec![]; - fmt.visit_contract(&mut contract)? - } - ParseSource::Function(mut func) => { - func.body = None; - fmt.visit_function(&mut func)? - } - ParseSource::Variable(mut var) => fmt.visit_var_definition(&mut var)?, - ParseSource::Event(mut event) => fmt.visit_event(&mut event)?, - ParseSource::Error(mut error) => fmt.visit_error(&mut error)?, - ParseSource::Struct(mut structure) => fmt.visit_struct(&mut structure)?, - ParseSource::Enum(mut enumeration) => fmt.visit_enum(&mut enumeration)?, - ParseSource::Type(mut ty) => fmt.visit_type_definition(&mut ty)?, - }; - - self.code = code; + // TODO(rusowsky): ensure that this is equivalent to the old fmt output + self.code = + forge_fmt::format(source, config).into_result().map_err(ParserError::Formatter)?; Ok(self) } diff --git a/crates/doc/src/parser/mod.rs b/crates/doc/src/parser/mod.rs index c87dfe8274dce..396c141f77317 100644 --- a/crates/doc/src/parser/mod.rs +++ b/crates/doc/src/parser/mod.rs @@ -1,6 +1,7 @@ //! The parser module. -use forge_fmt::{FormatterConfig, Visitable, Visitor}; +use crate::solang_ext::{Visitable, Visitor}; +use foundry_config::FormatterConfig; use itertools::Itertools; use solang_parser::{ doccomment::{DocComment, parse_doccomments}, @@ -295,7 +296,7 @@ mod tests { struct ContractStruct { } enum ContractEnum { } - uint256 constant CONTRACT_CONSTANT; + uint256 constant CONTRACT_CONSTANT = 0; bool contractVar; function contractFunction(uint256) external returns (uint256) { @@ -352,15 +353,15 @@ mod tests { pragma solidity ^0.8.19; /// @name Test /// no tag - ///@notice Cool contract - /// @ dev This is not a dev tag + ///@notice Cool contract + /// @ dev This is not a dev tag /** * @dev line one * line 2 */ contract Test { - /*** my function - i like whitespace + /*** my function + i like whitespace */ function test() {} } diff --git a/crates/doc/src/preprocessor/contract_inheritance.rs b/crates/doc/src/preprocessor/contract_inheritance.rs index 832d44e55fed9..6d4195700bcc4 100644 --- a/crates/doc/src/preprocessor/contract_inheritance.rs +++ b/crates/doc/src/preprocessor/contract_inheritance.rs @@ -1,7 +1,8 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{Document, ParseSource, PreprocessorOutput, document::DocumentContent}; +use crate::{ + Document, ParseSource, PreprocessorOutput, document::DocumentContent, solang_ext::SafeUnwrap, +}; use alloy_primitives::map::HashMap; -use forge_fmt::solang_ext::SafeUnwrap; use std::path::PathBuf; /// [ContractInheritance] preprocessor id. diff --git a/crates/doc/src/preprocessor/infer_hyperlinks.rs b/crates/doc/src/preprocessor/infer_hyperlinks.rs index 1477998642a82..9fd799127843c 100644 --- a/crates/doc/src/preprocessor/infer_hyperlinks.rs +++ b/crates/doc/src/preprocessor/infer_hyperlinks.rs @@ -1,6 +1,5 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{Comments, Document, ParseItem, ParseSource}; -use forge_fmt::solang_ext::SafeUnwrap; +use crate::{Comments, Document, ParseItem, ParseSource, solang_ext::SafeUnwrap}; use regex::{Captures, Match, Regex}; use std::{ borrow::Cow, diff --git a/crates/doc/src/preprocessor/inheritdoc.rs b/crates/doc/src/preprocessor/inheritdoc.rs index fd70483824452..9d02337389df7 100644 --- a/crates/doc/src/preprocessor/inheritdoc.rs +++ b/crates/doc/src/preprocessor/inheritdoc.rs @@ -1,9 +1,9 @@ use super::{Preprocessor, PreprocessorId}; use crate::{ Comments, Document, ParseItem, ParseSource, PreprocessorOutput, document::DocumentContent, + solang_ext::SafeUnwrap, }; use alloy_primitives::map::HashMap; -use forge_fmt::solang_ext::SafeUnwrap; /// [`Inheritdoc`] preprocessor ID. pub const INHERITDOC_ID: PreprocessorId = PreprocessorId("inheritdoc"); diff --git a/crates/fmt/src/solang_ext/ast_eq.rs b/crates/doc/src/solang_ext/ast_eq.rs similarity index 100% rename from crates/fmt/src/solang_ext/ast_eq.rs rename to crates/doc/src/solang_ext/ast_eq.rs diff --git a/crates/fmt/src/solang_ext/loc.rs b/crates/doc/src/solang_ext/loc.rs similarity index 100% rename from crates/fmt/src/solang_ext/loc.rs rename to crates/doc/src/solang_ext/loc.rs diff --git a/crates/fmt/src/solang_ext/mod.rs b/crates/doc/src/solang_ext/mod.rs similarity index 96% rename from crates/fmt/src/solang_ext/mod.rs rename to crates/doc/src/solang_ext/mod.rs index 3aa7c526c5ba1..d85dd1a5aace7 100644 --- a/crates/fmt/src/solang_ext/mod.rs +++ b/crates/doc/src/solang_ext/mod.rs @@ -23,7 +23,9 @@ pub mod pt { mod ast_eq; mod loc; mod safe_unwrap; +mod visit; pub use ast_eq::AstEq; pub use loc::CodeLocationExt; pub use safe_unwrap::SafeUnwrap; +pub use visit::{Visitable, Visitor}; diff --git a/crates/fmt/src/solang_ext/safe_unwrap.rs b/crates/doc/src/solang_ext/safe_unwrap.rs similarity index 100% rename from crates/fmt/src/solang_ext/safe_unwrap.rs rename to crates/doc/src/solang_ext/safe_unwrap.rs diff --git a/crates/fmt/src/visit.rs b/crates/doc/src/solang_ext/visit.rs similarity index 98% rename from crates/fmt/src/visit.rs rename to crates/doc/src/solang_ext/visit.rs index 86eeec7d57b62..80cb4f3dc1376 100644 --- a/crates/fmt/src/visit.rs +++ b/crates/doc/src/solang_ext/visit.rs @@ -287,18 +287,6 @@ pub trait Visitor { Ok(()) } - fn visit_opening_paren(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_closing_paren(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn visit_newline(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - fn visit_using(&mut self, using: &mut Using) -> Result<(), Self::Error> { self.visit_source(using.loc)?; self.visit_stray_semicolon()?; diff --git a/crates/doc/src/writer/as_doc.rs b/crates/doc/src/writer/as_doc.rs index 92f56b8068e89..bf86313a64d2c 100644 --- a/crates/doc/src/writer/as_doc.rs +++ b/crates/doc/src/writer/as_doc.rs @@ -3,9 +3,9 @@ use crate::{ GIT_SOURCE_ID, INHERITDOC_ID, Markdown, PreprocessorOutput, document::{DocumentContent, read_context}, parser::ParseSource, + solang_ext::SafeUnwrap, writer::BufWriter, }; -use forge_fmt::solang_ext::SafeUnwrap; use itertools::Itertools; use solang_parser::pt::{Base, FunctionDefinition}; use std::path::{Path, PathBuf}; diff --git a/crates/fmt-2/Cargo.toml b/crates/fmt-2/Cargo.toml deleted file mode 100644 index 35da06b882c03..0000000000000 --- a/crates/fmt-2/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "forge-fmt-2" - -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[lints] -workspace = true - -[dependencies] -foundry-config.workspace = true -foundry-common.workspace = true - -solar.workspace = true - -# alloy-primitives.workspace = true -itertools.workspace = true -similar = { version = "2", features = ["inline"] } -tracing.workspace = true -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } - -[dev-dependencies] -itertools.workspace = true -similar-asserts.workspace = true -toml.workspace = true -tracing-subscriber = { workspace = true, features = ["env-filter"] } -snapbox.workspace = true diff --git a/crates/fmt-2/README.md b/crates/fmt-2/README.md deleted file mode 100644 index 5db721344f222..0000000000000 --- a/crates/fmt-2/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Formatter (`fmt`) - -Solidity formatter that respects (some parts of) -the [Style Guide](https://docs.soliditylang.org/en/latest/style-guide.html) and -is tested on the [Prettier Solidity Plugin](https://github.com/prettier-solidity/prettier-plugin-solidity) cases. - -## Architecture - -The formatter's is built on top of [Solar](https://github.com/paradigmxyz/solar), and the architecture is based on a Wadler-style pretty-printing engine. The formatting process consists of two main steps: - -1. **Parsing**: The Solidity source code is parsed using **`solar`** into an **Abstract Syntax Tree (AST)**. The AST is a tree representation of the code's syntactic structure. -2. **Printing**: The AST is traversed by a visitor, which generates a stream of abstract tokens that are then processed by a pretty-printing engine to produce the final formatted code. - -### The Pretty Printer (`pp`) - -The core of the formatter is a pretty-printing engine inspired by Philip Wadler's algorithm, and adapted from the implementations in `rustc_ast_pretty` and `prettyplease`. Its goal is to produce an optimal and readable layout by making intelligent decisions about line breaks. - -The process works like this: - -1. **AST to Abstract Tokens**: The formatter's `State` object walks the `solar` AST. Instead of directly writing strings, it translates the AST nodes into a stream of abstract formatting "commands" called `Token`s. This decouples the code's structure from the final text output. The primary tokens are: - * **`String`**: An atomic, unbreakable piece of text, like a keyword (`function`), an identifier (`myVar`), or a literal (`42`). - * **`Break`**: A potential line break. This is the core of the engine's flexibility. The `Printer` later decides whether to render a `Break` as a single space or as a newline with appropriate indentation. - * **`Begin`/`End`**: These tokens define a logical group of tokens that should be formatted as a single unit. This allows the printer to decide how to format the entire group at once. - -2. **Grouping and Breaking Strategy**: The `Begin` and `End` tokens create formatting "boxes" that guide the breaking strategy. There are two main types of boxes: - * **Consistent Box (`cbox`)**: If *any* `Break` inside this box becomes a newline, then *all* `Break`s inside it must also become newlines. This is ideal for lists like function parameters or struct fields, ensuring they are either all on one line or neatly arranged with one item per line. - * **Inconsistent Box (`ibox`)**: `Break`s within this box are independent. The printer can wrap a long line at any `Break` point without forcing other breaks in the same box to become newlines. This is useful for formatting long expressions or comments. - -3. **The `Printer` Engine**: The `Printer` consumes this stream of tokens and makes the final decisions: - * It maintains a buffer of tokens and tracks the remaining space on the current line based on the configured `line_length`. - * When it encounters a `Begin` token for a group, it calculates whether the entire group could fit on the current line if all its `Break`s were spaces. - * **If it fits**, all `Break`s in that group are rendered as spaces. - * **If it doesn't fit**, `Break`s are rendered as newlines, and the indentation level is adjusted accordingly based on the box's rules (consistent or inconsistent). - -Crucially, this entire process is deterministic. Because the formatter completely rebuilds the code from the AST, it discards all original whitespace, line breaks, and other stylistic variations. This means that for a given AST and configuration, the output will always be identical. No matter how inconsistently the input code is formatted, the result is a single, canonical representation, ensuring predictability and consistency across any codebase. - -> **Debug Mode**: To visualize the debug output, and understand how the pretty-printer makes its decisions about boxes and breaks, see the [Debug](#debug) section in Testing. - -### Comments - -Comment handling is a critical aspect of the formatter, designed to preserve developer intent while restructuring the code. - -1. **Categorization**: Comments are parsed and categorized by their position and style: `Isolated` (on its own line), `Mixed` (on a line with code), and `Trailing` (at the end of a line). - -2. **Blank Line Handling**: Blank lines in the source code are treated as a special `BlankLine` comment type, allowing the formatter to preserve vertical spacing that separates logical blocks of code. However, to maintain a clean and consistent vertical rhythm, any sequence of multiple blank lines is collapsed into a single blank line. This prevents excessive empty space in the formatted output. - -3. **Integration with Printing**: During the AST traversal, the formatter queries for comments that appear before the current code element. These comments, including blank lines, are then strategically inserted into the `Printer`'s token stream. The formatter inserts `Break` tokens around comments to ensure they are correctly spaced from the surrounding code, and emits one or two `hardbreak`s for blank lines to maintain the original vertical rhythm. - -This approach allows the formatter to respect both the syntactic structure of the code and the developer's textual annotations and spacing, producing a clean, readable, and intentional layout. - -### Example - -**Source Code** -```solidity -pragma solidity ^0.8.10 ; -contract HelloWorld { - string public message; - constructor( string memory initMessage) { message = initMessage;} -} - - - -event Greet( string indexed name) ; -``` - -**Abstract Syntax Tree (AST) (simplified)** -```text -SourceUnit - ├─ PragmaDirective("solidity", "^0.8.10") - ├─ ItemContract("HelloWorld") - │ ├─ VariableDefinition { name: "message", ty: "string", visibility: "public" } - │ └─ ItemFunction { - │ kind: Constructor, - │ header: FunctionHeader { - │ parameters: [ - │ VariableDefinition { name: "initMessage", ty: "string", data_location: "memory" } - │ ] - │ }, - │ body: Block { - │ stmts: [ - │ Stmt { kind: Expr(Assign {lhs: Ident("message"), rhs: Ident("initMessage")}) } - │ ] - │ } - │ } - └─ ItemEvent { name: "Greet", parameters: [ - VariableDefinition { name: "name", ty: "string", indexed: true } - ] } -``` - - -**Formatted Source Code** -The code is reconstructed from the AST using the pretty-printer. -```solidity -pragma solidity ^0.8.10; - -contract HelloWorld { - string public message; - - constructor(string memory initMessage) { - message = initMessage; - } -} - -event Greet(string indexed name); -``` - -### Configuration - -The formatter supports multiple configuration options defined in `foundry.toml`. - -| Option | Default | Description | -| :--- | :--- | :--- | -| `line_length` | `120` | Maximum line length where the formatter will try to wrap the line. | -| `tab_width` | `4` | Number of spaces per indentation level. Ignored if `style` is `tab`. | -| `style` | `space` | The style of indentation. Options: `space`, `tab`. | -| `bracket_spacing` | `false` | Print spaces between brackets. | -| `int_types` | `long` | Style for `uint256`/`int256` types. Options: `long`, `short`, `preserve`. | -| `multiline_func_header` | `attributes_first` | The style of multiline function headers. Options: `attributes_first`, `params_first`, `params_first_multi`, `all`, `all_params`. | -| `quote_style` | `double` | The style of quotation marks. Options: `double`, `single`, `preserve`. | -| `number_underscore` | `preserve` | The style of underscores in number literals. Options: `preserve`, `remove`, `thousands`. | -| `hex_underscore` | `remove` | The style of underscores in hex literals. Options: `preserve`, `remove`, `bytes`. | -| `single_line_statement_blocks` | `preserve` | The style of single-line blocks in statements. Options: `preserve`, `single`, `multi`. | -| `override_spacing` | `false` | Print a space in the `override` attribute. | -| `wrap_comments` | `false` | Wrap comments when `line_length` is reached. | -| `ignore` | `[]` | Globs to ignore. | -| `contract_new_lines` | `false` | Add a new line at the start and end of contract declarations. | -| `sort_imports` | `false` | Sort import statements alphabetically in groups. A group is a set of imports separated by a newline. | -| `pow_no_space` | `false` | Suppress spaces around the power operator (`**`). | - - -### Inline Configuration - -The formatter can be instructed to skip specific sections of code using inline comments. While the tool supports fine-grained control, it is generally more robust and efficient to disable formatting for entire AST items or statements. - -This approach is preferred because it allows the formatter to treat the entire disabled item as a single, opaque unit. It can simply copy the original source text for that item's span instead of partially formatting a line, switching to copy mode, and then resuming formatting. This leads to more predictable output and avoids potential edge cases with complex, partially-disabled statements. - -#### Disable Line - -These directives are best used when they apply to a complete, self-contained AST statement, as shown below. In this case, `uint x = 100;` is a full statement, making it a good candidate for a line-based disable. - -To disable the next line: -```solidity -// forgefmt: disable-next-line -uint x = 100; -``` - -To disable the current line: -```solidity -uint x = 100; // forgefmt: disable-line -``` -#### Disable Block - -This is the recommended approach for complex, multi-line constructs where you want to preserve specific formatting. In the example below, the entire `function` definition is disabled. This is preferable to trying to disable individual lines within the signature, because lines like `uint256 b /* a comment that goes inside the comma */,` do not correspond to a complete AST item or statement on their own. Disabling the whole item is cleaner and more aligned with the code's structure. - -```solidity -// forgefmt: disable-start -function fnWithManyArguments( - uint a, - uint256 b /* a comment that goes inside the comma */, - uint256 c -) external returns (bool) { -// forgefmt: disable-end -``` - -## Contributing - -Check out the [foundry contribution guide](https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md). - -Guidelines for contributing to `forge fmt`: - -### Opening an issue - -1. Create a short, concise title describing the issue. - * **Bad**: `Forge fmt does not work` - * **Good**: `bug(forge-fmt): misplaces postfix comment on if-statement` -2. Fill in the issue template fields, including Foundry version, platform, and component info. -3. Provide code snippets showing the current and expected behaviors. -4. If it's a feature request, explain why the feature is needed. -5. Add the `C-forge` and `Cmd-forge-fmt` labels. - -### Fixing a Bug or Developing a Feature - -1. Specify the issue being addressed in the PR description. -2. Add a note on your solution in the PR description. -3. Ensure the PR includes comprehensive acceptance tests under `fmt/testdata/`, covering: - * The specific case being fixed/added. - * Behavior with different kinds of comments (isolated, mixed, trailing). - * If it's a new config value, tests covering all available options. - -### Testing - -Tests are located in the `fmt/testdata` folder. Each test consists of an `original.sol` file and one or more expected output files, named `*.fmt.sol`. - -The default configuration can be overridden from within an expected output file by adding a comment in the format `// config: {config_key} = {config_value}`. For example: -```solidity -// config: line_length = 160 -``` - -The testing process for each test suite is as follows: -1. Read `original.sol` and the corresponding `*.fmt.sol` expected output. -2. Parse any `// config:` comments from the expected file to create a test-specific configuration. -3. Format `original.sol` and assert that the output matches the content of `*.fmt.sol`. -4. To ensure **idempotency**, format the content of `*.fmt.sol` again and assert that the output does not change. - -### Debug - -The formatter includes a debug mode that provides visual insight into the pretty-printer's decision-making process. This is invaluable for troubleshooting complex formatting issues and understanding how the boxes and breaks described in [The Pretty Printer](#the-pretty-printer-pp) section work. - -To enable it, run the formatter with the `FMT_DEBUG` environment variable set: -```sh -FMT_DEBUG=1 cargo test -p forge-fmt-2 --test formatter Repros -``` - -When enabled, the output will be annotated with special characters representing the printer's internal state: - -* **Boxes**: - * `«` and `»`: Mark the start and end of a **consistent** box (`cbox`). - * `‹` and `›`: Mark the start and end of an **inconsistent** box (`ibox`). - -* **Breaks**: - * `·`: Represents a `Break` token, which could be a space or a newline. - -For example, running debug mode on the `HelloWorld` contract from earlier would produce an output like this: - -```text -pragma solidity ^0.8.10;· -· -«‹«contract HelloWorld »{›· -‹‹ string· public· message››;· -· -« constructor«(«‹‹string memory initMessage››»)» {»· -«‹ message = ·initMessage›·;· -» }· -»}· -· -event Greet(««‹‹string indexed name››»»);· -``` - -This annotated output allows you to see exactly how the printer is grouping tokens and where it considers inserting a space or a newline. This makes it much easier to diagnose why a certain layout is being produced. diff --git a/crates/fmt-2/src/lib.rs b/crates/fmt-2/src/lib.rs deleted file mode 100644 index fee58d47ebc77..0000000000000 --- a/crates/fmt-2/src/lib.rs +++ /dev/null @@ -1,282 +0,0 @@ -#![doc = include_str!("../README.md")] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![allow(dead_code)] // TODO(dani) - -const DEBUG: bool = false || option_env!("FMT_DEBUG").is_some(); -const DEBUG_INDENT: bool = false; - -use foundry_common::comments::{ - Comment, Comments, - inline_config::{InlineConfig, InlineConfigItem}, -}; - -// TODO(dani) -// #[macro_use] -// extern crate tracing; -use tracing as _; -use tracing_subscriber as _; - -mod state; - -mod pp; - -use solar::{ - parse::{ - ast::{SourceUnit, Span}, - interface::{Session, diagnostics::EmittedDiagnostics, source_map::SourceFile}, - }, - sema::Compiler, -}; - -use std::{path::Path, sync::Arc}; - -pub use foundry_config::fmt::*; - -/// The result of the formatter. -pub type FormatterResult = DiagnosticsResult; - -/// The result of the formatter. -#[derive(Debug)] -pub enum DiagnosticsResult { - /// Everything went well. - Ok(T), - /// No errors encountered, but warnings or other non-error diagnostics were emitted. - OkWithDiagnostics(T, E), - /// Errors encountered, but a result was produced anyway. - ErrRecovered(T, E), - /// Fatal errors encountered. - Err(E), -} - -impl DiagnosticsResult { - /// Converts the formatter result into a standard result. - /// - /// This ignores any non-error diagnostics if `Ok`, and any valid result if `Err`. - pub fn into_result(self) -> Result { - match self { - Self::Ok(s) | Self::OkWithDiagnostics(s, _) => Ok(s), - Self::ErrRecovered(_, d) | Self::Err(d) => Err(d), - } - } - - /// Returns the result, even if it was produced with errors. - pub fn into_ok(self) -> Result { - match self { - Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Ok(s), - Self::Err(e) => Err(e), - } - } - - /// Returns any result produced. - pub fn ok_ref(&self) -> Option<&T> { - match self { - Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Some(s), - Self::Err(_) => None, - } - } - - /// Returns any diagnostics emitted. - pub fn err_ref(&self) -> Option<&E> { - match self { - Self::Ok(_) => None, - Self::OkWithDiagnostics(_, d) | Self::ErrRecovered(_, d) | Self::Err(d) => Some(d), - } - } - - /// Returns `true` if the result is `Ok`. - pub fn is_ok(&self) -> bool { - matches!(self, Self::Ok(_) | Self::OkWithDiagnostics(_, _)) - } - - /// Returns `true` if the result is `Err`. - pub fn is_err(&self) -> bool { - !self.is_ok() - } -} - -pub fn format_file( - path: &Path, - config: FormatterConfig, - compiler: &mut Compiler, -) -> FormatterResult { - format_inner(config, compiler, &|sess| { - sess.source_map().load_file(path).map_err(|e| sess.dcx.err(e.to_string()).emit()) - }) -} - -pub fn format_source( - source: &str, - path: Option<&Path>, - config: FormatterConfig, - compiler: &mut Compiler, -) -> FormatterResult { - format_inner(config, compiler, &|sess| { - let name = match path { - Some(path) => solar::parse::interface::source_map::FileName::Real(path.to_path_buf()), - None => solar::parse::interface::source_map::FileName::Stdin, - }; - sess.source_map() - .new_source_file(name, source) - .map_err(|e| sess.dcx.err(e.to_string()).emit()) - }) -} - -fn format_inner( - config: FormatterConfig, - compiler: &mut Compiler, - mk_file: &(dyn Fn(&Session) -> solar::parse::interface::Result> + Sync + Send), -) -> FormatterResult { - // First pass formatting - let first_result = format_once(config.clone(), compiler, mk_file); - - // If first pass was not successful, return the result - if first_result.is_err() { - return first_result; - } - let Some(first_formatted) = first_result.ok_ref() else { return first_result }; - - // Second pass formatting - let second_result = format_once(config, compiler, &|sess| { - sess.source_map() - .new_source_file( - solar::parse::interface::source_map::FileName::Custom("format-again".to_string()), - first_formatted, - ) - .map_err(|e| sess.dcx.err(e.to_string()).emit()) - }); - - // Check if the two passes produce the same output (idempotency) - match (first_result.ok_ref(), second_result.ok_ref()) { - (Some(first), Some(second)) if first != second => { - panic!("formatter is not idempotent:\n{}", diff(first, second)); - } - _ => {} - } - - if first_result.is_ok() && second_result.is_err() && !DEBUG { - panic!( - "failed to format a second time:\nfirst_result={first_result:#?}\nsecond_result={second_result:#?}" - ); - // second_result - } else { - first_result - } -} - -fn diff(first: &str, second: &str) -> impl std::fmt::Display { - use std::fmt::Write; - let diff = similar::TextDiff::from_lines(first, second); - let mut s = String::new(); - for change in diff.iter_all_changes() { - let tag = match change.tag() { - similar::ChangeTag::Delete => "-", - similar::ChangeTag::Insert => "+", - similar::ChangeTag::Equal => " ", - }; - write!(s, "{tag}{change}").unwrap(); - } - s -} - -fn format_once( - config: FormatterConfig, - compiler: &mut Compiler, - mk_file: &( - dyn Fn(&solar::interface::Session) -> solar::interface::Result> - + Send - + Sync - ), -) -> FormatterResult { - let res = compiler.enter_mut(|c| -> solar::interface::Result { - let mut pcx = c.parse(); - pcx.set_resolve_imports(false); - - let file = mk_file(c.sess())?; - let file_path = file.name.as_real(); - - if let Some(path) = file_path { - pcx.load_files(&[path.to_path_buf()])?; - } else { - // Fallback for non-file sources like stdin - pcx.add_file(file.to_owned()); - } - pcx.parse(); - - let gcx = c.gcx(); - // Iterate over `gcx.sources` to find the correct `SourceUnit` - let source = if let Some(path) = file_path { - gcx.sources.iter().find(|su| su.file.name.as_real() == Some(path)) - } else { - gcx.sources.first() - } - .ok_or_else(|| c.dcx().bug("no source file parsed").emit())?; - - let ast = source.ast.as_ref().ok_or_else(|| c.dcx().err("unable to read AST").emit())?; - - let comments = Comments::new( - &source.file, - c.sess().source_map(), - true, - config.wrap_comments, - if matches!(config.style, IndentStyle::Tab) { Some(config.tab_width) } else { None }, - ); - let inline_config = parse_inline_config(c.sess(), &comments, ast); - - let mut state = state::State::new(c.sess().source_map(), config, inline_config, comments); - state.print_source_unit(ast); - Ok(state.s.eof()) - }); - - let diagnostics = compiler.sess().dcx.emitted_diagnostics().unwrap(); - match (res, compiler.sess().dcx.has_errors()) { - (Ok(s), Ok(())) if diagnostics.is_empty() => FormatterResult::Ok(s), - (Ok(s), Ok(())) => FormatterResult::OkWithDiagnostics(s, diagnostics), - (Ok(s), Err(_)) => FormatterResult::ErrRecovered(s, diagnostics), - (Err(_), Ok(_)) => unreachable!(), - (Err(_), Err(_)) => FormatterResult::Err(diagnostics), - } -} - -fn parse_inline_config<'ast>( - sess: &Session, - comments: &Comments, - ast: &'ast SourceUnit<'ast>, -) -> InlineConfig<()> { - let parse_item = |mut item: &str, cmnt: &Comment| -> Option<(Span, InlineConfigItem<()>)> { - if let Some(prefix) = cmnt.prefix() { - item = item.strip_prefix(prefix).unwrap_or(item); - } - if let Some(suffix) = cmnt.suffix() { - item = item.strip_suffix(suffix).unwrap_or(item); - } - let item = item.trim_start().strip_prefix("forgefmt:")?.trim(); - match item.parse::>() { - Ok(item) => Some((cmnt.span, item)), - Err(e) => { - sess.dcx.warn(e.to_string()).span(cmnt.span).emit(); - None - } - } - }; - - let items = comments.iter().flat_map(|cmnt| { - let mut found_items = Vec::with_capacity(2); - // Always process the first line. - if let Some(line) = cmnt.lines.first() - && let Some(item) = parse_item(line, cmnt) - { - found_items.push(item); - } - // If the comment has more than one line, process the last line. - if cmnt.lines.len() > 1 - && let Some(line) = cmnt.lines.last() - && let Some(item) = parse_item(line, cmnt) - { - found_items.push(item); - } - found_items - }); - - InlineConfig::from_ast(items, ast, sess.source_map()) -} diff --git a/crates/fmt-2/tests/formatter.rs b/crates/fmt-2/tests/formatter.rs deleted file mode 100644 index ca23323315b1f..0000000000000 --- a/crates/fmt-2/tests/formatter.rs +++ /dev/null @@ -1,243 +0,0 @@ -use forge_fmt_2::FormatterConfig; -use snapbox::{Data, assert_data_eq}; -use solar::sema::Compiler; -use std::{ - fs, - path::{Path, PathBuf}, -}; -use tracing_subscriber::{EnvFilter, FmtSubscriber}; - -#[track_caller] -fn format(source: &str, path: &Path, fmt_config: FormatterConfig) -> String { - let mut compiler = Compiler::new( - solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(), - ); - - match forge_fmt_2::format_source(source, Some(path), fmt_config, &mut compiler).into_result() { - Ok(formatted) => formatted, - Err(e) => panic!("failed to format {path:?}: {e}"), - } -} - -#[track_caller] -fn assert_eof(content: &str) { - assert!(content.ends_with('\n'), "missing trailing newline"); - assert!(!content.ends_with("\n\n"), "extra trailing newline"); -} - -fn enable_tracing() { - let subscriber = FmtSubscriber::builder() - .with_env_filter(EnvFilter::from_default_env()) - .with_test_writer() - .finish(); - let _ = tracing::subscriber::set_global_default(subscriber); -} - -fn tests_dir() -> PathBuf { - // TODO: re-enable once `fmt-2` becomes `fmt` - // Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata") - Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("fmt").join("testdata") -} - -fn test_directory(base_name: &str) { - enable_tracing(); - let dir = tests_dir().join(base_name); - let mut original = fs::read_to_string(dir.join("original.sol")).unwrap(); - if cfg!(windows) { - original = original.replace("\r\n", "\n"); - } - let mut handles = vec![]; - for res in dir.read_dir().unwrap() { - let entry = res.unwrap(); - let path = entry.path(); - - let filename = path.file_name().and_then(|name| name.to_str()).unwrap(); - if filename == "original.sol" { - continue; - } - assert!(path.is_file(), "expected file: {path:?}"); - assert!(filename.ends_with("fmt.sol"), "unknown file: {path:?}"); - - let mut expected = fs::read_to_string(&path).unwrap(); - if cfg!(windows) { - expected = expected - .replace("\r\n", "\n") - .replace(r"\'", r"/'") - .replace(r#"\""#, r#"/""#) - .replace("\\\n", "/\n"); - } - - // The majority of the tests were written with the assumption that the default value for max - // line length is `80`. Preserve that to avoid rewriting test logic. - let default_config = FormatterConfig { line_length: 80, ..Default::default() }; - - let mut config = toml::Value::try_from(default_config).unwrap(); - let config_table = config.as_table_mut().unwrap(); - let mut comments_end = 0; - for (i, line) in expected.lines().enumerate() { - let line_num = i + 1; - let Some(entry) = line - .strip_prefix("//") - .and_then(|line| line.trim().strip_prefix("config:")) - .map(str::trim) - else { - break; - }; - - let values = match toml::from_str::(entry) { - Ok(toml::Value::Table(table)) => table, - r => panic!("invalid fmt config item in {filename} at {line_num}: {r:?}"), - }; - config_table.extend(values); - - comments_end += line.len() + 1; - } - let config = config - .try_into() - .unwrap_or_else(|err| panic!("invalid test config for {filename}: {err}")); - - let original = original.clone(); - let tname = format!("{base_name}/{filename}"); - let spawn = move || { - test_formatter(&path, config, &original, &expected, comments_end); - }; - handles.push(std::thread::Builder::new().name(tname).spawn(spawn).unwrap()); - } - let results = handles.into_iter().map(|h| h.join()).collect::>(); - for result in results { - result.unwrap(); - } -} - -fn test_formatter( - expected_path: &Path, - config: FormatterConfig, - source: &str, - expected_source: &str, - comments_end: usize, -) { - let path = &*expected_path.with_file_name("original.sol"); - let expected_data = || { - let data = Data::read_from(expected_path, None); - if cfg!(windows) { - let content = data - .to_string() - .replace("\r\n", "\n") - .replace(r"\'", r"/'") - .replace(r#"\""#, r#"/""#) - .replace("\\\n", "/\n"); - Data::text(content) - } else { - data.raw() - } - }; - - let mut source_formatted = format(source, path, config.clone()); - // Inject `expected`'s comments, if any, so we can use the expected file as a snapshot. - source_formatted.insert_str(0, &expected_source[..comments_end]); - assert_data_eq!(&source_formatted, expected_data()); - assert_eof(&source_formatted); - - let mut expected_content = std::fs::read_to_string(expected_path).unwrap(); - if cfg!(windows) { - expected_content = expected_content.replace("\r\n", "\n"); - } - let expected_formatted = format(&expected_content, expected_path, config); - assert_data_eq!(&expected_formatted, expected_data()); - assert_eof(expected_source); - assert_eof(&expected_formatted); -} - -fn test_all_dirs_are_declared(dirs: &[&str]) { - let mut undeclared = vec![]; - for actual_dir in tests_dir().read_dir().unwrap().filter_map(Result::ok) { - let path = actual_dir.path(); - assert!(path.is_dir(), "expected directory: {path:?}"); - let actual_dir_name = path.file_name().unwrap().to_str().unwrap(); - if !dirs.contains(&actual_dir_name) { - undeclared.push(actual_dir_name.to_string()); - } - } - if !undeclared.is_empty() { - panic!( - "the following test directories are not declared in the test suite macro call: {undeclared:#?}" - ); - } -} - -macro_rules! fmt_tests { - ($($(#[$attr:meta])* $dir:ident),+ $(,)?) => { - #[test] - fn all_dirs_are_declared() { - test_all_dirs_are_declared(&[$(stringify!($dir)),*]); - } - - $( - #[allow(non_snake_case)] - #[test] - $(#[$attr])* - fn $dir() { - test_directory(stringify!($dir)); - } - )+ - }; -} - -fmt_tests! { - #[ignore = "annotations are not valid Solidity"] - Annotation, - ArrayExpressions, - BlockComments, - BlockCommentsFunction, - ConditionalOperatorExpression, - ConstructorDefinition, - ConstructorModifierStyle, - ContractDefinition, - DocComments, - DoWhileStatement, - EmitStatement, - EnumDefinition, - EnumVariants, - ErrorDefinition, - EventDefinition, - ForStatement, - FunctionCall, - FunctionCallArgsStatement, - FunctionDefinition, - FunctionDefinitionWithFunctionReturns, - FunctionType, - HexUnderscore, - IfStatement, - IfStatement2, - ImportDirective, - InlineDisable, - IntTypes, - LiteralExpression, - MappingType, - ModifierDefinition, - NamedFunctionCallExpression, - NonKeywords, - NumberLiteralUnderscore, - OperatorExpressions, - PragmaDirective, - Repros, - ReturnStatement, - RevertNamedArgsStatement, - RevertStatement, - SimpleComments, - SortedImports, - StatementBlock, - StructDefinition, - ThisExpression, - #[ignore = "Solar errors when parsing inputs with trailing commas"] - TrailingComma, - TryStatement, - TypeDefinition, - UnitExpression, - UsingDirective, - VariableAssignment, - VariableDefinition, - WhileStatement, - Yul, - YulStrings, -} diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml index bc1f44fdc5e03..6c806587ae645 100644 --- a/crates/fmt/Cargo.toml +++ b/crates/fmt/Cargo.toml @@ -14,17 +14,19 @@ workspace = true [dependencies] foundry-config.workspace = true +foundry-common.workspace = true -alloy-primitives.workspace = true +solar.workspace = true -ariadne = "0.5" +# alloy-primitives.workspace = true itertools.workspace = true -solang-parser.workspace = true -thiserror.workspace = true +similar = { version = "2", features = ["inline"] } tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } [dev-dependencies] itertools.workspace = true similar-asserts.workspace = true toml.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } +snapbox.workspace = true diff --git a/crates/fmt/README.md b/crates/fmt/README.md index 014988bef019d..81515220e40c0 100644 --- a/crates/fmt/README.md +++ b/crates/fmt/README.md @@ -6,71 +6,51 @@ is tested on the [Prettier Solidity Plugin](https://github.com/prettier-solidity ## Architecture -The formatter works in two steps: +The formatter is built on top of [Solar](https://github.com/paradigmxyz/solar), and the architecture is based on a Wadler-style pretty-printing engine. The formatting process consists of two main steps: -1. Parse Solidity source code with [solang](https://github.com/hyperledger-labs/solang) into the PT (Parse Tree) - (not the same as Abstract Syntax Tree, [see difference](https://stackoverflow.com/a/9864571)). -2. Walk the PT and output new source code that's compliant with provided config and rule set. +1. **Parsing**: The Solidity source code is parsed using **`solar`** into an **Abstract Syntax Tree (AST)**. The AST is a tree representation of the code's syntactic structure. +2. **Printing**: The AST is traversed by a visitor, which generates a stream of abstract tokens that are then processed by a pretty-printing engine to produce the final formatted code. -The technique for walking the tree is based on [Visitor Pattern](https://en.wikipedia.org/wiki/Visitor_pattern) -and works as following: +### The Pretty Printer (`pp`) -1. Implement `Formatter` callback functions for each PT node type. - Every callback function should write formatted output for the current node - and call `Visitable::visit` function for child nodes delegating the output writing. -1. Implement `Visitable` trait and its `visit` function for each PT node type. Every `visit` function should call - corresponding `Formatter`'s callback function. +The core of the formatter is a pretty-printing engine inspired by Philip Wadler's algorithm, and adapted from the implementations in `rustc_ast_pretty` and `prettyplease`. Its goal is to produce an optimal and readable layout by making intelligent decisions about line breaks. -### Output +The process works like this: -The formatted output is written into the output buffer in _chunks_. The `Chunk` struct holds the content to be written & -metadata for it. This includes the comments surrounding the content as well as the `needs_space` flag specifying whether -this _chunk_ needs a space. The flag overrides the default behavior of `Formatter::next_char_needs_space` method. +1. **AST to Abstract Tokens**: The formatter's `State` object walks the `solar` AST. Instead of directly writing strings, it translates the AST nodes into a stream of abstract formatting "commands" called `Token`s. This decouples the code's structure from the final text output. The primary tokens are: + * **`String`**: An atomic, unbreakable piece of text, like a keyword (`function`), an identifier (`myVar`), or a literal (`42`). + * **`Break`**: A potential line break. This is the core of the engine's flexibility. The `Printer` later decides whether to render a `Break` as a single space or as a newline with appropriate indentation. + * **`Begin`/`End`**: These tokens define a logical group of tokens that should be formatted as a single unit. This allows the printer to decide how to format the entire group at once. -The content gets written into the `FormatBuffer` which contains the information about the current indentation level, -indentation length, current state as well as the other data determining the rules for writing the content. -`FormatBuffer` implements the `std::fmt::Write` trait where it evaluates the current information and decides how the -content should be written to the destination. +2. **Grouping and Breaking Strategy**: The `Begin` and `End` tokens create formatting "boxes" that guide the breaking strategy. There are two main types of boxes: + * **Consistent Box (`cbox`)**: If *any* `Break` inside this box becomes a newline, then *all* `Break`s inside it must also become newlines. This is ideal for lists like function parameters or struct fields, ensuring they are either all on one line or neatly arranged with one item per line. + * **Inconsistent Box (`ibox`)**: `Break`s within this box are independent. The printer can wrap a long line at any `Break` point without forcing other breaks in the same box to become newlines. This is useful for formatting long expressions or comments. -### Comments +3. **The `Printer` Engine**: The `Printer` consumes this stream of tokens and makes the final decisions: + * It maintains a buffer of tokens and tracks the remaining space on the current line based on the configured `line_length`. + * When it encounters a `Begin` token for a group, it calculates whether the entire group could fit on the current line if all its `Break`s were spaces. + * **If it fits**, all `Break`s in that group are rendered as spaces. + * **If it doesn't fit**, `Break`s are rendered as newlines, and the indentation level is adjusted accordingly based on the box's rules (consistent or inconsistent). -The solang parser does not output comments as a type of parse tree node, but rather -in a list alongside the parse tree with location information. It is therefore necessary -to infer where to insert the comments and how to format them while traversing the parse tree. +Crucially, this entire process is deterministic. Because the formatter completely rebuilds the code from the AST, it discards all original whitespace, line breaks, and other stylistic variations. This means that for a given AST and configuration, the output will always be identical. No matter how inconsistently the input code is formatted, the result is a single, canonical representation, ensuring predictability and consistency across any codebase. -To handle this, the formatter pre-parses the comments and puts them into two categories: -Prefix and Postfix comments. Prefix comments refer to the node directly after them, and -postfix comments refer to the node before them. As an illustration: +> **Debug Mode**: To visualize the debug output, and understand how the pretty-printer makes its decisions about boxes and breaks, see the [Debug](#debug) section in Testing. -```solidity -// This is a prefix comment -/* This is also a prefix comment */ -uint variable = 1 + 2; /* this is postfix */ // this is postfix too - // and this is a postfix comment on the next line -``` +### Comments -To insert the comments into the appropriate areas, strings get converted to chunks -before being written to the buffer. A chunk is any string that cannot be split by -whitespace. A chunk also carries with it the surrounding comment information. Thereby -when writing the chunk the comments can be added before and after the chunk as well -as any whitespace surrounding. +Comment handling is a critical aspect of the formatter, designed to preserve developer intent while restructuring the code. -To construct a chunk, the string and the location of the string is given to the -Formatter and the pre-parsed comments before the start and end of the string are -associated with that string. The source code can then further be chunked before the -chunks are written to the buffer. +1. **Categorization**: Comments are parsed and categorized by their position and style: `Isolated` (on its own line), `Mixed` (on a line with code), and `Trailing` (at the end of a line). -To write the chunk, first the comments associated with the start of the chunk get -written to the buffer. Then the Formatter checks if any whitespace is needed between -what's been written to the buffer and what's in the chunk and inserts it where appropriate. -If the chunk content fits on the same line, it will be written directly to the buffer, -otherwise it will be written on the next line. Finally, any associated postfix -comments also get written. +2. **Blank Line Handling**: Blank lines in the source code are treated as a special `BlankLine` comment type, allowing the formatter to preserve vertical spacing that separates logical blocks of code. However, to maintain a clean and consistent vertical rhythm, any sequence of multiple blank lines is collapsed into a single blank line. This prevents excessive empty space in the formatted output. -### Example +3. **Integration with Printing**: During the AST traversal, the formatter queries for comments that appear before the current code element. These comments, including blank lines, are then strategically inserted into the `Printer`'s token stream. The formatter inserts `Break` tokens around comments to ensure they are correctly spaced from the surrounding code, and emits one or two `hardbreak`s for blank lines to maintain the original vertical rhythm. -Source code +This approach allows the formatter to respect both the syntactic structure of the code and the developer's textual annotations and spacing, producing a clean, readable, and intentional layout. +### Example + +**Source Code** ```solidity pragma solidity ^0.8.10 ; contract HelloWorld { @@ -79,23 +59,37 @@ contract HelloWorld { } + event Greet( string indexed name) ; ``` -Parse Tree (simplified) - +**Abstract Syntax Tree (AST) (simplified)** ```text SourceUnit - | PragmaDirective("solidity", "^0.8.10") - | ContractDefinition("HelloWorld") - | VariableDefinition("string", "message", null, ["public"]) - | FunctionDefinition("constructor") - | Parameter("string", "initMessage", ["memory"]) - | EventDefinition("string", "Greet", ["indexed"], ["name"]) + ├─ PragmaDirective("solidity", "^0.8.10") + ├─ ItemContract("HelloWorld") + │ ├─ VariableDefinition { name: "message", ty: "string", visibility: "public" } + │ └─ ItemFunction { + │ kind: Constructor, + │ header: FunctionHeader { + │ parameters: [ + │ VariableDefinition { name: "initMessage", ty: "string", data_location: "memory" } + │ ] + │ }, + │ body: Block { + │ stmts: [ + │ Stmt { kind: Expr(Assign {lhs: Ident("message"), rhs: Ident("initMessage")}) } + │ ] + │ } + │ } + └─ ItemEvent { name: "Greet", parameters: [ + VariableDefinition { name: "name", ty: "string", indexed: true } + ] } ``` -Formatted source code that was reconstructed from the Parse Tree +**Formatted Source Code** +The code is reconstructed from the AST using the pretty-printer. ```solidity pragma solidity ^0.8.10; @@ -112,112 +106,134 @@ event Greet(string indexed name); ### Configuration -The formatter supports multiple configuration options defined in `FormatterConfig`. - -| Option | Default | Description | -| ---------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| line_length | 120 | Maximum line length where formatter will try to wrap the line | -| tab_width | 4 | Number of spaces per indentation level | -| bracket_spacing | false | Print spaces between brackets | -| int_types | long | Style of uint/int256 types. Available options: `long`, `short`, `preserve` | -| multiline_func_header | attributes_first | Style of multiline function header in case it doesn't fit. Available options: `params_first`, `params_first_multi`, `attributes_first`, `all`, `all_params` | -| quote_style | double | Style of quotation marks. Available options: `double`, `single`, `preserve` | -| number_underscore | preserve | Style of underscores in number literals. Available options: `preserve`, `remove`, `thousands` | -| hex_underscore | remove | Style of underscores in hex literals. Available options: `preserve`, `remove`, `bytes` | -| single_line_statement_blocks | preserve | Style of single line blocks in statements. Available options: `single`, `multi`, `preserve` | -| override_spacing | false | Print space in state variable, function and modifier `override` attribute | -| wrap_comments | false | Wrap comments on `line_length` reached | -| ignore | [] | Globs to ignore | -| contract_new_lines | false | Add new line at start and end of contract declarations | -| sort_imports | false | Sort import statements alphabetically in groups | -| style | space | Configures if spaces or tabs should be used for indents. `tab_width` will be ignored if set to `tab`. Available options: `space`, `tab` | - -### Disable Line - -The formatter can be disabled on specific lines by adding a comment `// forgefmt: disable-next-line`, like this: +The formatter supports multiple configuration options defined in `foundry.toml`. + +| Option | Default | Description | +| :--- | :--- | :--- | +| `line_length` | `120` | Maximum line length where the formatter will try to wrap the line. | +| `tab_width` | `4` | Number of spaces per indentation level. Ignored if `style` is `tab`. | +| `style` | `space` | The style of indentation. Options: `space`, `tab`. | +| `bracket_spacing` | `false` | Print spaces between brackets. | +| `int_types` | `long` | Style for `uint256`/`int256` types. Options: `long`, `short`, `preserve`. | +| `multiline_func_header` | `attributes_first` | The style of multiline function headers. Options: `attributes_first`, `params_first`, `params_first_multi`, `all`, `all_params`. | +| `quote_style` | `double` | The style of quotation marks. Options: `double`, `single`, `preserve`. | +| `number_underscore` | `preserve` | The style of underscores in number literals. Options: `preserve`, `remove`, `thousands`. | +| `hex_underscore` | `remove` | The style of underscores in hex literals. Options: `preserve`, `remove`, `bytes`. | +| `single_line_statement_blocks` | `preserve` | The style of single-line blocks in statements. Options: `preserve`, `single`, `multi`. | +| `override_spacing` | `false` | Print a space in the `override` attribute. | +| `wrap_comments` | `false` | Wrap comments when `line_length` is reached. | +| `ignore` | `[]` | Globs to ignore. | +| `contract_new_lines` | `false` | Add a new line at the start and end of contract declarations. | +| `sort_imports` | `false` | Sort import statements alphabetically in groups. A group is a set of imports separated by a newline. | +| `pow_no_space` | `false` | Suppress spaces around the power operator (`**`). | + +### Inline Configuration + +The formatter can be instructed to skip specific sections of code using inline comments. While the tool supports fine-grained control, it is generally more robust and efficient to disable formatting for entire AST items or statements. + +This approach is preferred because it allows the formatter to treat the entire disabled item as a single, opaque unit. It can simply copy the original source text for that item's span instead of partially formatting a line, switching to copy mode, and then resuming formatting. This leads to more predictable output and avoids potential edge cases with complex, partially-disabled statements. + +#### Disable Line + +These directives are best used when they apply to a complete, self-contained AST statement, as shown below. In this case, `uint x = 100;` is a full statement, making it a good candidate for a line-based disable. + +To disable the next line: ```solidity // forgefmt: disable-next-line uint x = 100; ``` -Alternatively, the comment can also be placed at the end of the line. In this case, you'd have to use `disable-line` -instead: - +To disable the current line: ```solidity uint x = 100; // forgefmt: disable-line ``` +#### Disable Block -### Disable Block - -The formatter can be disabled for a section of code by adding a comment `// forgefmt: disable-start` before and a -comment `// forgefmt: disable-end` after, like this: +This is the recommended approach for complex, multi-line constructs where you want to preserve specific formatting. In the example below, the entire `function` definition is disabled. This is preferable to trying to disable individual lines within the signature, because lines like `uint256 b /* a comment that goes inside the comma */,` do not correspond to a complete AST item or statement on their own. Disabling the whole item is cleaner and more aligned with the code's structure. ```solidity // forgefmt: disable-start -uint x = 100; -uint y = 101; +function fnWithManyArguments( + uint a, + uint256 b /* a comment that goes inside the comma */, + uint256 c +) external returns (bool) { // forgefmt: disable-end ``` -### Testing +## Contributing -Tests reside under the `fmt/testdata` folder and specify the malformatted & expected Solidity code. The source code file -is named `original.sol` and expected file(s) are named in a format `({prefix}.)?fmt.sol`. Multiple expected files are -needed for tests covering available configuration options. +Check out the [foundry contribution guide](https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md). + +Guidelines for contributing to `forge fmt`: + +### Opening an issue + +1. Create a short, concise title describing the issue. + * **Bad**: `Forge fmt does not work` + * **Good**: `bug(forge-fmt): misplaces postfix comment on if-statement` +2. Fill in the issue template fields, including Foundry version, platform, and component info. +3. Provide code snippets showing the current and expected behaviors. +4. If it's a feature request, explain why the feature is needed. +5. Add the `C-forge` and `Cmd-forge-fmt` labels. + +### Fixing a Bug or Developing a Feature + +1. Specify the issue being addressed in the PR description. +2. Add a note on your solution in the PR description. +3. Ensure the PR includes comprehensive acceptance tests under `fmt/testdata/`, covering: + * The specific case being fixed/added. + * Behavior with different kinds of comments (isolated, mixed, trailing). + * If it's a new config value, tests covering all available options. + +### Testing -The default configuration values can be overridden from within the expected file by adding a comment in the format -`// config: {config_entry} = {config_value}`. For example: +Tests are located in the `fmt/testdata` folder. Each test consists of an `original.sol` file and one or more expected output files, named `*.fmt.sol`. +The default configuration can be overridden from within an expected output file by adding a comment in the format `// config: {config_key} = {config_value}`. For example: ```solidity // config: line_length = 160 ``` -The `test_directory` macro is used to specify a new folder with source files for the test suite. Each test suite has the -following process: +The testing process for each test suite is as follows: +1. Read `original.sol` and the corresponding `*.fmt.sol` expected output. +2. Parse any `// config:` comments from the expected file to create a test-specific configuration. +3. Format `original.sol` and assert that the output matches the content of `*.fmt.sol`. +4. To ensure **idempotency**, format the content of `*.fmt.sol` again and assert that the output does not change. -1. Preparse comments with config values -2. Parse and compare the AST for source & expected files. - - The `AstEq` trait defines the comparison rules for the AST nodes -3. Format the source file and assert the equality of the output with the expected file. -4. Format the expected files and assert the idempotency of the formatting operation. +### Debug -## Contributing +The formatter includes a debug mode that provides visual insight into the pretty-printer's decision-making process. This is invaluable for troubleshooting complex formatting issues and understanding how the boxes and breaks described in [The Pretty Printer](#the-pretty-printer-pp) section work. -Check out the [foundry contribution guide](https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md). +To enable it, run the formatter with the `FMT_DEBUG` environment variable set: +```sh +FMT_DEBUG=1 cargo test -p forge-fmt --test formatter Repros +``` -Guidelines for contributing to `forge fmt`: +When enabled, the output will be annotated with special characters representing the printer's internal state: -### Opening an issue +* **Boxes**: + * `«` and `»`: Mark the start and end of a **consistent** box (`cbox`). + * `‹` and `›`: Mark the start and end of an **inconsistent** box (`ibox`). + +* **Breaks**: + * `·`: Represents a `Break` token, which could be a space or a newline. + +For example, running debug mode on the `HelloWorld` contract from earlier would produce an output like this: + +```text +pragma solidity ^0.8.10;· +· +«‹«contract HelloWorld »{›· +‹‹ string· public· message››;· +· +« constructor«(«‹‹string memory initMessage››»)» {»· +«‹ message = ·initMessage›·;· +» }· +»}· +· +event Greet(««‹‹string indexed name››»»);· +``` -1. Create a short concise title describing an issue. - - Bad Title Examples - ```text - Forge fmt does not work - Forge fmt breaks - Forge fmt unexpected behavior - ``` - - Good Title Examples - ```text - Forge fmt postfix comment misplaced - Forge fmt does not inline short yul blocks - ``` -2. Fill in the issue template fields that include foundry version, platform & component info. -3. Provide the code snippets showing the current & expected behaviors. -4. If it's a feature request, specify why this feature is needed. -5. Besides the default label (`T-Bug` for bugs or `T-feature` for features), add `C-forge` and `Cmd-forge-fmt` labels. - -### Fixing A Bug - -1. Specify an issue that is being addressed in the PR description. -2. Add a note on the solution in the PR description. -3. Make sure the PR includes the acceptance test(s). - -### Developing A Feature - -1. Specify an issue that is being addressed in the PR description. -2. Add a note on the solution in the PR description. -3. Provide the test coverage for the new feature. These should include: - - Adding malformatted & expected solidity code under `fmt/testdata/$dir/` - - Testing the behavior of pre and postfix comments - - If it's a new config value, tests covering **all** available options +This annotated output allows you to see exactly how the printer is grouping tokens and where it considers inserting a space or a newline. This makes it much easier to diagnose why a certain layout is being produced. diff --git a/crates/fmt/src/buffer.rs b/crates/fmt/src/buffer.rs deleted file mode 100644 index c9281faed4a09..0000000000000 --- a/crates/fmt/src/buffer.rs +++ /dev/null @@ -1,497 +0,0 @@ -//! Format buffer. - -use crate::{ - comments::{CommentState, CommentStringExt}, - string::{QuoteState, QuotedStringExt}, -}; -use foundry_config::fmt::IndentStyle; -use std::fmt::Write; - -/// An indent group. The group may optionally skip the first line -#[derive(Clone, Debug, Default)] -struct IndentGroup { - skip_line: bool, -} - -#[derive(Clone, Copy, Debug)] -enum WriteState { - LineStart(CommentState), - WriteTokens(CommentState), - WriteString(char), -} - -impl WriteState { - fn comment_state(&self) -> CommentState { - match self { - Self::LineStart(state) => *state, - Self::WriteTokens(state) => *state, - Self::WriteString(_) => CommentState::None, - } - } -} - -impl Default for WriteState { - fn default() -> Self { - Self::LineStart(CommentState::default()) - } -} - -/// A wrapper around a `std::fmt::Write` interface. The wrapper keeps track of indentation as well -/// as information about the last `write_str` command if available. The formatter may also be -/// restricted to a single line, in which case it will throw an error on a newline -#[derive(Clone, Debug)] -pub struct FormatBuffer { - pub w: W, - indents: Vec, - base_indent_len: usize, - tab_width: usize, - style: IndentStyle, - last_char: Option, - current_line_len: usize, - restrict_to_single_line: bool, - state: WriteState, -} - -impl FormatBuffer { - pub fn new(w: W, tab_width: usize, style: IndentStyle) -> Self { - Self { - w, - tab_width, - style, - base_indent_len: 0, - indents: vec![], - current_line_len: 0, - last_char: None, - restrict_to_single_line: false, - state: WriteState::default(), - } - } - - /// Create a new temporary buffer based on an existing buffer which retains information about - /// the buffer state, but has a blank String as its underlying `Write` interface - pub fn create_temp_buf(&self) -> FormatBuffer { - let mut new = FormatBuffer::new(String::new(), self.tab_width, self.style); - new.base_indent_len = self.total_indent_len(); - new.current_line_len = self.current_line_len(); - new.last_char = self.last_char; - new.restrict_to_single_line = self.restrict_to_single_line; - new.state = match self.state { - WriteState::WriteTokens(state) | WriteState::LineStart(state) => { - WriteState::LineStart(state) - } - WriteState::WriteString(ch) => WriteState::WriteString(ch), - }; - new - } - - /// Restrict the buffer to a single line - pub fn restrict_to_single_line(&mut self, restricted: bool) { - self.restrict_to_single_line = restricted; - } - - /// Indent the buffer by delta - pub fn indent(&mut self, delta: usize) { - self.indents.extend(std::iter::repeat_n(IndentGroup::default(), delta)); - } - - /// Dedent the buffer by delta - pub fn dedent(&mut self, delta: usize) { - self.indents.truncate(self.indents.len() - delta); - } - - /// Get the current level of the indent. This is multiplied by the tab width to get the - /// resulting indent - fn level(&self) -> usize { - self.indents.iter().filter(|i| !i.skip_line).count() - } - - /// Check if the last indent group is being skipped - pub fn last_indent_group_skipped(&self) -> bool { - self.indents.last().map(|i| i.skip_line).unwrap_or(false) - } - - /// Set whether the last indent group should be skipped - pub fn set_last_indent_group_skipped(&mut self, skip_line: bool) { - if let Some(i) = self.indents.last_mut() { - i.skip_line = skip_line - } - } - - /// Get the current indent size. level * tab_width for spaces and level for tabs - pub fn current_indent_len(&self) -> usize { - match self.style { - IndentStyle::Space => self.level() * self.tab_width, - IndentStyle::Tab => self.level(), - } - } - - /// Get the char used for indent - pub fn indent_char(&self) -> char { - match self.style { - IndentStyle::Space => ' ', - IndentStyle::Tab => '\t', - } - } - - /// Get the indent len for the given level - pub fn get_indent_len(&self, level: usize) -> usize { - match self.style { - IndentStyle::Space => level * self.tab_width, - IndentStyle::Tab => level, - } - } - - /// Get the total indent size - pub fn total_indent_len(&self) -> usize { - self.current_indent_len() + self.base_indent_len - } - - /// Get the current written position (this does not include the indent size) - pub fn current_line_len(&self) -> usize { - self.current_line_len - } - - /// Check if the buffer is at the beginning of a new line - pub fn is_beginning_of_line(&self) -> bool { - matches!(self.state, WriteState::LineStart(_)) - } - - /// Start a new indent group (skips first indent) - pub fn start_group(&mut self) { - self.indents.push(IndentGroup { skip_line: true }); - } - - /// End the last indent group - pub fn end_group(&mut self) { - self.indents.pop(); - } - - /// Get the last char written to the buffer - pub fn last_char(&self) -> Option { - self.last_char - } - - /// When writing a newline apply state changes - fn handle_newline(&mut self, mut comment_state: CommentState) { - if comment_state == CommentState::Line { - comment_state = CommentState::None; - } - self.current_line_len = 0; - self.set_last_indent_group_skipped(false); - self.last_char = Some('\n'); - self.state = WriteState::LineStart(comment_state); - } -} - -impl FormatBuffer { - /// Write a raw string to the buffer. This will ignore indents and remove the indents of the - /// written string to match the current base indent of this buffer if it is a temp buffer - pub fn write_raw(&mut self, s: impl AsRef) -> std::fmt::Result { - self._write_raw(s.as_ref()) - } - - fn _write_raw(&mut self, s: &str) -> std::fmt::Result { - let mut lines = s.lines().peekable(); - let mut comment_state = self.state.comment_state(); - while let Some(line) = lines.next() { - // remove the whitespace that covered by the base indent length (this is normally the - // case with temporary buffers as this will be re-added by the underlying IndentWriter - // later on - let (new_comment_state, line_start) = line - .comment_state_char_indices() - .with_state(comment_state) - .take(self.base_indent_len) - .take_while(|(_, _, ch)| ch.is_whitespace()) - .last() - .map(|(state, idx, ch)| (state, idx + ch.len_utf8())) - .unwrap_or((comment_state, 0)); - comment_state = new_comment_state; - let trimmed_line = &line[line_start..]; - if !trimmed_line.is_empty() { - self.w.write_str(trimmed_line)?; - self.current_line_len += trimmed_line.len(); - self.last_char = trimmed_line.chars().next_back(); - self.state = WriteState::WriteTokens(comment_state); - } - if lines.peek().is_some() || s.ends_with('\n') { - if self.restrict_to_single_line { - return Err(std::fmt::Error); - } - self.w.write_char('\n')?; - self.handle_newline(comment_state); - } - } - Ok(()) - } -} - -impl Write for FormatBuffer { - fn write_str(&mut self, mut s: &str) -> std::fmt::Result { - if s.is_empty() { - return Ok(()); - } - - let mut indent = self.indent_char().to_string().repeat(self.current_indent_len()); - - loop { - match self.state { - WriteState::LineStart(mut comment_state) => { - match s.find(|b| b != '\n') { - // No non-empty lines in input, write the entire string (only newlines) - None => { - if !s.is_empty() { - self.w.write_str(s)?; - self.handle_newline(comment_state); - } - break; - } - - // We can see the next non-empty line. Write up to the - // beginning of that line, then insert an indent, then - // continue. - Some(len) => { - let (head, tail) = s.split_at(len); - self.w.write_str(head)?; - self.w.write_str(&indent)?; - self.current_line_len = 0; - self.last_char = Some(self.indent_char()); - // a newline has been inserted - if len > 0 { - if self.last_indent_group_skipped() { - indent = self - .indent_char() - .to_string() - .repeat(self.get_indent_len(self.level() + 1)); - self.set_last_indent_group_skipped(false); - } - if comment_state == CommentState::Line { - comment_state = CommentState::None; - } - } - s = tail; - self.state = WriteState::WriteTokens(comment_state); - } - } - } - WriteState::WriteTokens(comment_state) => { - if s.is_empty() { - break; - } - - // find the next newline or non-comment string separator (e.g. ' or ") - let mut len = 0; - let mut new_state = WriteState::WriteTokens(comment_state); - for (state, idx, ch) in s.comment_state_char_indices().with_state(comment_state) - { - len = idx; - if ch == '\n' { - if self.restrict_to_single_line { - return Err(std::fmt::Error); - } - new_state = WriteState::LineStart(state); - break; - } else if state == CommentState::None && (ch == '\'' || ch == '"') { - new_state = WriteState::WriteString(ch); - break; - } else { - new_state = WriteState::WriteTokens(state); - } - } - - if matches!(new_state, WriteState::WriteTokens(_)) { - // No newlines or strings found, write the entire string - self.w.write_str(s)?; - self.current_line_len += s.len(); - self.last_char = s.chars().next_back(); - self.state = new_state; - break; - } else { - // A newline or string has been found. Write up to that character and - // continue on the tail - let (head, tail) = s.split_at(len + 1); - self.w.write_str(head)?; - s = tail; - match new_state { - WriteState::LineStart(comment_state) => { - self.handle_newline(comment_state) - } - new_state => { - self.current_line_len += head.len(); - self.last_char = head.chars().next_back(); - self.state = new_state; - } - } - } - } - WriteState::WriteString(quote) => { - match s.quoted_ranges().with_state(QuoteState::String(quote)).next() { - // No end found, write the rest of the string - None => { - self.w.write_str(s)?; - self.current_line_len += s.len(); - self.last_char = s.chars().next_back(); - break; - } - // String end found, write the string and continue to add tokens after - Some((_, _, len)) => { - let (head, tail) = s.split_at(len + 1); - self.w.write_str(head)?; - if let Some((_, last)) = head.rsplit_once('\n') { - self.set_last_indent_group_skipped(false); - self.current_line_len = last.len(); - } else { - self.current_line_len += head.len(); - } - self.last_char = Some(quote); - s = tail; - self.state = WriteState::WriteTokens(CommentState::None); - } - } - } - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const TAB_WIDTH: usize = 4; - - #[test] - fn test_buffer_indents() -> std::fmt::Result { - let delta = 1; - - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); - assert_eq!(buf.indents.len(), 0); - assert_eq!(buf.level(), 0); - assert_eq!(buf.current_indent_len(), 0); - assert_eq!(buf.style, IndentStyle::Space); - - buf.indent(delta); - assert_eq!(buf.indents.len(), delta); - assert_eq!(buf.level(), delta); - assert_eq!(buf.current_indent_len(), delta * TAB_WIDTH); - - buf.indent(delta); - buf.set_last_indent_group_skipped(true); - assert!(buf.last_indent_group_skipped()); - assert_eq!(buf.indents.len(), delta * 2); - assert_eq!(buf.level(), delta); - assert_eq!(buf.current_indent_len(), delta * TAB_WIDTH); - buf.dedent(delta); - - buf.dedent(delta); - assert_eq!(buf.indents.len(), 0); - assert_eq!(buf.level(), 0); - assert_eq!(buf.current_indent_len(), 0); - - // panics on extra dedent - let res = std::panic::catch_unwind(|| buf.clone().dedent(delta)); - assert!(res.is_err()); - - Ok(()) - } - - #[test] - fn test_identical_temp_buf() -> std::fmt::Result { - let content = "test string"; - let multiline_content = "test\nmultiline\nmultiple"; - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); - - // create identical temp buf - let mut temp = buf.create_temp_buf(); - writeln!(buf, "{content}")?; - writeln!(temp, "{content}")?; - assert_eq!(buf.w, format!("{content}\n")); - assert_eq!(temp.w, buf.w); - assert_eq!(temp.current_line_len, buf.current_line_len); - assert_eq!(temp.base_indent_len, buf.total_indent_len()); - - let delta = 1; - buf.indent(delta); - - let mut temp_indented = buf.create_temp_buf(); - assert!(temp_indented.w.is_empty()); - assert_eq!(temp_indented.base_indent_len, buf.total_indent_len()); - assert_eq!(temp_indented.level() + delta, buf.level()); - - let indent = " ".repeat(delta * TAB_WIDTH); - - let mut original_buf = buf.clone(); - write!(buf, "{multiline_content}")?; - let expected_content = format!( - "{}\n{}{}", - content, - indent, - multiline_content.lines().collect::>().join(&format!("\n{indent}")) - ); - assert_eq!(buf.w, expected_content); - - write!(temp_indented, "{multiline_content}")?; - - // write temp buf to original and assert the result - write!(original_buf, "{}", temp_indented.w)?; - assert_eq!(buf.w, original_buf.w); - - Ok(()) - } - - #[test] - fn test_preserves_original_content_with_default_settings() -> std::fmt::Result { - let contents = [ - "simple line", - r" - some - multiline - content", - "// comment", - "/* comment */", - r"mutliline - content - // comment1 - with comments - /* comment2 */ ", - ]; - - for content in &contents { - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); - write!(buf, "{content}")?; - assert_eq!(&buf.w, content); - } - - Ok(()) - } - - #[test] - fn test_indent_char() -> std::fmt::Result { - assert_eq!( - FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space).indent_char(), - ' ' - ); - assert_eq!( - FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Tab).indent_char(), - '\t' - ); - Ok(()) - } - - #[test] - fn test_indent_len() -> std::fmt::Result { - // Space should use level * TAB_WIDTH - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); - assert_eq!(buf.current_indent_len(), 0); - buf.indent(2); - assert_eq!(buf.current_indent_len(), 2 * TAB_WIDTH); - - // Tab should use level - buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Tab); - assert_eq!(buf.current_indent_len(), 0); - buf.indent(2); - assert_eq!(buf.current_indent_len(), 2); - Ok(()) - } -} diff --git a/crates/fmt/src/chunk.rs b/crates/fmt/src/chunk.rs deleted file mode 100644 index 7d9ce25c7fbd0..0000000000000 --- a/crates/fmt/src/chunk.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::comments::CommentWithMetadata; - -/// Holds information about a non-whitespace-splittable string, and the surrounding comments -#[derive(Clone, Debug, Default)] -pub struct Chunk { - pub postfixes_before: Vec, - pub prefixes: Vec, - pub content: String, - pub postfixes: Vec, - pub needs_space: Option, -} - -impl From for Chunk { - fn from(string: String) -> Self { - Self { content: string, ..Default::default() } - } -} - -impl From<&str> for Chunk { - fn from(string: &str) -> Self { - Self { content: string.to_owned(), ..Default::default() } - } -} - -// The struct with information about chunks used in the [Formatter::surrounded] method -#[derive(Debug)] -pub struct SurroundingChunk { - pub before: Option, - pub next: Option, - pub spaced: Option, - pub content: String, -} - -impl SurroundingChunk { - pub fn new( - content: impl std::fmt::Display, - before: Option, - next: Option, - ) -> Self { - Self { before, next, content: format!("{content}"), spaced: None } - } - - pub fn spaced(mut self) -> Self { - self.spaced = Some(true); - self - } - - pub fn non_spaced(mut self) -> Self { - self.spaced = Some(false); - self - } - - pub fn loc_before(&self) -> usize { - self.before.unwrap_or_default() - } - - pub fn loc_next(&self) -> Option { - self.next - } -} diff --git a/crates/fmt/src/comments.rs b/crates/fmt/src/comments.rs deleted file mode 100644 index cb69181315005..0000000000000 --- a/crates/fmt/src/comments.rs +++ /dev/null @@ -1,456 +0,0 @@ -use crate::inline_config::{InlineConfigItem, InvalidInlineConfigItem}; -use itertools::Itertools; -use solang_parser::pt::*; -use std::collections::VecDeque; - -/// The type of a Comment -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CommentType { - /// A Line comment (e.g. `// ...`) - Line, - /// A Block comment (e.g. `/* ... */`) - Block, - /// A Doc Line comment (e.g. `/// ...`) - DocLine, - /// A Doc Block comment (e.g. `/** ... */`) - DocBlock, -} - -/// The comment position -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CommentPosition { - /// Comes before the code it describes - Prefix, - /// Comes after the code it describes - Postfix, -} - -/// Comment with additional metadata -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CommentWithMetadata { - pub ty: CommentType, - pub loc: Loc, - pub has_newline_before: bool, - pub indent_len: usize, - pub comment: String, - pub position: CommentPosition, -} - -impl PartialOrd for CommentWithMetadata { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CommentWithMetadata { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.loc.cmp(&other.loc) - } -} - -impl CommentWithMetadata { - fn new( - comment: Comment, - position: CommentPosition, - has_newline_before: bool, - indent_len: usize, - ) -> Self { - let (ty, loc, comment) = match comment { - Comment::Line(loc, comment) => (CommentType::Line, loc, comment), - Comment::Block(loc, comment) => (CommentType::Block, loc, comment), - Comment::DocLine(loc, comment) => (CommentType::DocLine, loc, comment), - Comment::DocBlock(loc, comment) => (CommentType::DocBlock, loc, comment), - }; - Self { - comment: comment.trim_end().to_string(), - ty, - loc, - position, - has_newline_before, - indent_len, - } - } - - /// Construct a comment with metadata by analyzing its surrounding source code - fn from_comment_and_src(comment: Comment, src: &str, last_comment: Option<&Self>) -> Self { - let src_before = &src[..comment.loc().start()]; - if src_before.is_empty() { - return Self::new(comment, CommentPosition::Prefix, false, 0); - } - - let mut lines_before = src_before.lines().rev(); - let this_line = - if src_before.ends_with('\n') { "" } else { lines_before.next().unwrap_or_default() }; - let indent_len = this_line.chars().take_while(|c| c.is_whitespace()).count(); - let last_line = lines_before.next().map(str::trim_start); - - if matches!(comment, Comment::DocLine(..) | Comment::DocBlock(..)) { - return Self::new( - comment, - CommentPosition::Prefix, - last_line.is_none_or(str::is_empty), - indent_len, - ); - } - - // TODO: this loop takes almost the entirety of the time spent in parsing, which is up to - // 80% of `crate::fmt` - let mut code_end = 0; - for (state, idx, ch) in src_before.comment_state_char_indices() { - if matches!(state, CommentState::None) && !ch.is_whitespace() { - code_end = idx; - } - } - - let (position, has_newline_before) = if src_before[code_end..].contains('\n') { - // comment sits on a line without code - if let Some(last_line) = last_line { - if last_line.is_empty() { - // line before is empty - (CommentPosition::Prefix, true) - } else { - // line has something - // check if the last comment after code was a postfix comment - if last_comment - .is_some_and(|last| last.loc.end() > code_end && !last.is_prefix()) - { - // get the indent size of the next item of code - let next_indent_len = src[comment.loc().end()..] - .non_comment_chars() - .take_while(|ch| ch.is_whitespace()) - .fold(indent_len, |indent, ch| if ch == '\n' { 0 } else { indent + 1 }); - if indent_len > next_indent_len { - // the comment indent is bigger than the next code indent - (CommentPosition::Postfix, false) - } else { - // the comment indent is equal to or less than the next code - // indent - (CommentPosition::Prefix, false) - } - } else { - // if there is no postfix comment after the piece of code - (CommentPosition::Prefix, false) - } - } - } else { - // beginning of file - (CommentPosition::Prefix, false) - } - } else { - // comment is after some code - (CommentPosition::Postfix, false) - }; - - Self::new(comment, position, has_newline_before, indent_len) - } - - pub fn is_line(&self) -> bool { - matches!(self.ty, CommentType::Line | CommentType::DocLine) - } - - pub fn is_doc_block(&self) -> bool { - matches!(self.ty, CommentType::DocBlock) - } - - pub fn is_prefix(&self) -> bool { - matches!(self.position, CommentPosition::Prefix) - } - - pub fn is_before(&self, byte: usize) -> bool { - self.loc.start() < byte - } - - /// Returns the contents of the comment without the start and end tokens - pub fn contents(&self) -> &str { - let mut s = self.comment.as_str(); - if let Some(stripped) = s.strip_prefix(self.start_token()) { - s = stripped; - } - if let Some(end_token) = self.end_token() - && let Some(stripped) = s.strip_suffix(end_token) - { - s = stripped; - } - s - } - - /// The start token of the comment - #[inline] - pub const fn start_token(&self) -> &'static str { - match self.ty { - CommentType::Line => "//", - CommentType::Block => "/*", - CommentType::DocLine => "///", - CommentType::DocBlock => "/**", - } - } - - /// The token that gets written on the newline when the - /// comment is wrapped - #[inline] - pub const fn wrap_token(&self) -> &'static str { - match self.ty { - CommentType::Line => "// ", - CommentType::DocLine => "/// ", - CommentType::Block => "", - CommentType::DocBlock => " * ", - } - } - - /// The end token of the comment - #[inline] - pub const fn end_token(&self) -> Option<&'static str> { - match self.ty { - CommentType::Line | CommentType::DocLine => None, - CommentType::Block | CommentType::DocBlock => Some("*/"), - } - } -} - -/// A list of comments -#[derive(Clone, Debug, Default)] -pub struct Comments { - prefixes: VecDeque, - postfixes: VecDeque, -} - -impl Comments { - pub fn new(mut comments: Vec, src: &str) -> Self { - let mut prefixes = VecDeque::with_capacity(comments.len()); - let mut postfixes = VecDeque::with_capacity(comments.len()); - let mut last_comment = None; - - comments.sort_by_key(|comment| comment.loc()); - for comment in comments { - let comment = CommentWithMetadata::from_comment_and_src(comment, src, last_comment); - let vec = if comment.is_prefix() { &mut prefixes } else { &mut postfixes }; - vec.push_back(comment); - last_comment = Some(vec.back().unwrap()); - } - Self { prefixes, postfixes } - } - - /// Helper for removing comments before a byte offset - fn remove_comments_before( - comments: &mut VecDeque, - byte: usize, - ) -> Vec { - let pos = comments - .iter() - .find_position(|comment| !comment.is_before(byte)) - .map(|(idx, _)| idx) - .unwrap_or_else(|| comments.len()); - if pos == 0 { - return Vec::new(); - } - comments.rotate_left(pos); - comments.split_off(comments.len() - pos).into() - } - - /// Remove any prefix comments that occur before the byte offset in the src - pub(crate) fn remove_prefixes_before(&mut self, byte: usize) -> Vec { - Self::remove_comments_before(&mut self.prefixes, byte) - } - - /// Remove any postfix comments that occur before the byte offset in the src - pub(crate) fn remove_postfixes_before(&mut self, byte: usize) -> Vec { - Self::remove_comments_before(&mut self.postfixes, byte) - } - - /// Remove any comments that occur before the byte offset in the src - pub(crate) fn remove_all_comments_before(&mut self, byte: usize) -> Vec { - self.remove_prefixes_before(byte) - .into_iter() - .merge(self.remove_postfixes_before(byte)) - .collect() - } - - pub(crate) fn pop(&mut self) -> Option { - if self.iter().next()?.is_prefix() { - self.prefixes.pop_front() - } else { - self.postfixes.pop_front() - } - } - - pub(crate) fn iter(&self) -> impl Iterator { - self.prefixes.iter().merge(self.postfixes.iter()) - } - - /// Parse all comments to return a list of inline config items. This will return an iterator of - /// results of parsing comments which start with `forgefmt:` - pub fn parse_inline_config_items( - &self, - ) -> impl Iterator> + '_ - { - self.iter() - .filter_map(|comment| { - Some((comment, comment.contents().trim_start().strip_prefix("forgefmt:")?.trim())) - }) - .map(|(comment, item)| { - let loc = comment.loc; - item.parse().map(|out| (loc, out)).map_err(|out| (loc, out)) - }) - } -} - -/// The state of a character in a string with possible comments -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum CommentState { - /// character not in a comment - #[default] - None, - /// First `/` in line comment start `"//"` - LineStart1, - /// Second `/` in line comment start `"//"` - LineStart2, - /// Character in a line comment - Line, - /// `/` in block comment start `"/*"` - BlockStart1, - /// `*` in block comment start `"/*"` - BlockStart2, - /// Character in a block comment - Block, - /// `*` in block comment end `"*/"` - BlockEnd1, - /// `/` in block comment end `"*/"` - BlockEnd2, -} - -/// An Iterator over characters and indices in a string slice with information about the state of -/// comments -pub struct CommentStateCharIndices<'a> { - iter: std::str::CharIndices<'a>, - state: CommentState, -} - -impl<'a> CommentStateCharIndices<'a> { - #[inline] - fn new(string: &'a str) -> Self { - Self { iter: string.char_indices(), state: CommentState::None } - } - - #[inline] - pub fn with_state(mut self, state: CommentState) -> Self { - self.state = state; - self - } - - #[inline] - pub fn peek(&mut self) -> Option<(usize, char)> { - self.iter.clone().next() - } -} - -impl Iterator for CommentStateCharIndices<'_> { - type Item = (CommentState, usize, char); - - #[inline] - fn next(&mut self) -> Option { - let (idx, ch) = self.iter.next()?; - match self.state { - CommentState::None => { - if ch == '/' { - self.state = match self.peek() { - Some((_, '/')) => CommentState::LineStart1, - Some((_, '*')) => CommentState::BlockStart1, - _ => CommentState::None, - }; - } - } - CommentState::LineStart1 => { - self.state = CommentState::LineStart2; - } - CommentState::LineStart2 => { - self.state = CommentState::Line; - } - CommentState::Line => { - if ch == '\n' { - self.state = CommentState::None; - } - } - CommentState::BlockStart1 => { - self.state = CommentState::BlockStart2; - } - CommentState::BlockStart2 => { - self.state = CommentState::Block; - } - CommentState::Block => { - if ch == '*' - && let Some((_, '/')) = self.peek() - { - self.state = CommentState::BlockEnd1; - } - } - CommentState::BlockEnd1 => { - self.state = CommentState::BlockEnd2; - } - CommentState::BlockEnd2 => { - self.state = CommentState::None; - } - } - Some((self.state, idx, ch)) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.iter.size_hint() - } - - #[inline] - fn count(self) -> usize { - self.iter.count() - } -} - -impl std::iter::FusedIterator for CommentStateCharIndices<'_> {} - -/// An Iterator over characters in a string slice which are not a part of comments -pub struct NonCommentChars<'a>(CommentStateCharIndices<'a>); - -impl Iterator for NonCommentChars<'_> { - type Item = char; - - #[inline] - fn next(&mut self) -> Option { - for (state, _, ch) in self.0.by_ref() { - if state == CommentState::None { - return Some(ch); - } - } - None - } -} - -/// Helpers for iterating over comment containing strings -pub trait CommentStringExt { - fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_>; - - #[inline] - fn non_comment_chars(&self) -> NonCommentChars<'_> { - NonCommentChars(self.comment_state_char_indices()) - } - - #[inline] - fn trim_comments(&self) -> String { - self.non_comment_chars().collect() - } -} - -impl CommentStringExt for T -where - T: AsRef, -{ - #[inline] - fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_> { - CommentStateCharIndices::new(self.as_ref()) - } -} - -impl CommentStringExt for str { - #[inline] - fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_> { - CommentStateCharIndices::new(self) - } -} diff --git a/crates/fmt/src/formatter.rs b/crates/fmt/src/formatter.rs deleted file mode 100644 index c0fcbdfc65fb8..0000000000000 --- a/crates/fmt/src/formatter.rs +++ /dev/null @@ -1,3895 +0,0 @@ -//! A Solidity formatter - -use crate::{ - FormatterConfig, InlineConfig, IntTypes, - buffer::*, - chunk::*, - comments::{ - CommentPosition, CommentState, CommentStringExt, CommentType, CommentWithMetadata, Comments, - }, - format_diagnostics_report, - helpers::import_path_string, - macros::*, - solang_ext::{pt::*, *}, - string::{QuoteState, QuotedStringExt}, - visit::{Visitable, Visitor}, -}; -use alloy_primitives::Address; -use foundry_config::fmt::{HexUnderscore, MultilineFuncHeaderStyle, SingleLineBlockStyle}; -use itertools::{Either, Itertools}; -use solang_parser::diagnostics::Diagnostic; -use std::{fmt::Write, path::PathBuf, str::FromStr}; -use thiserror::Error; - -type Result = std::result::Result; - -/// A custom Error thrown by the Formatter -#[derive(Debug, Error)] -pub enum FormatterError { - /// Error thrown by `std::fmt::Write` interfaces - #[error(transparent)] - Fmt(#[from] std::fmt::Error), - /// Encountered invalid parse tree item. - #[error("encountered invalid parse tree item at {0:?}")] - InvalidParsedItem(Loc), - /// Failed to parse the source code - #[error("failed to parse file:\n{}", format_diagnostics_report(_0, _1.as_deref(), _2))] - Parse(String, Option, Vec), - /// All other errors - #[error(transparent)] - Custom(Box), -} - -impl FormatterError { - fn fmt() -> Self { - Self::Fmt(std::fmt::Error) - } - - fn custom(err: impl std::error::Error + Send + Sync + 'static) -> Self { - Self::Custom(Box::new(err)) - } -} - -#[expect(unused_macros)] -macro_rules! format_err { - ($msg:literal $(,)?) => { - $crate::formatter::FormatterError::custom($msg.to_string()) - }; - ($err:expr $(,)?) => { - $crate::formatter::FormatterError::custom($err) - }; - ($fmt:expr, $($arg:tt)*) => { - $crate::formatter::FormatterError::custom(format!($fmt, $($arg)*)) - }; -} - -macro_rules! bail { - ($msg:literal $(,)?) => { - return Err($crate::formatter::format_err!($msg)) - }; - ($err:expr $(,)?) => { - return Err($err) - }; - ($fmt:expr, $($arg:tt)*) => { - return Err($crate::formatter::format_err!($fmt, $(arg)*)) - }; -} - -// TODO: store context entities as references without copying -/// Current context of the Formatter (e.g. inside Contract or Function definition) -#[derive(Debug, Default)] -struct Context { - contract: Option, - function: Option, - if_stmt_single_line: Option, -} - -impl Context { - /// Returns true if the current function context is the constructor - pub(crate) fn is_constructor_function(&self) -> bool { - self.function.as_ref().is_some_and(|f| matches!(f.ty, FunctionTy::Constructor)) - } -} - -/// A Solidity formatter -#[derive(Debug)] -pub struct Formatter<'a, W> { - buf: FormatBuffer, - source: &'a str, - config: FormatterConfig, - temp_bufs: Vec>, - context: Context, - comments: Comments, - inline_config: InlineConfig, -} - -impl<'a, W: Write> Formatter<'a, W> { - pub fn new( - w: W, - source: &'a str, - comments: Comments, - inline_config: InlineConfig, - config: FormatterConfig, - ) -> Self { - Self { - buf: FormatBuffer::new(w, config.tab_width, config.style), - source, - config, - temp_bufs: Vec::new(), - context: Context::default(), - comments, - inline_config, - } - } - - /// Get the Write interface of the current temp buffer or the underlying Write - fn buf(&mut self) -> &mut dyn Write { - match &mut self.temp_bufs[..] { - [] => &mut self.buf as &mut dyn Write, - [.., buf] => buf as &mut dyn Write, - } - } - - /// Casts the current writer `w` as a `String` reference. Should only be used for debugging. - unsafe fn buf_contents(&self) -> &String { - unsafe { *(&raw const self.buf.w as *const &mut String) } - } - - /// Casts the current `W` writer or the current temp buffer as a `String` reference. - /// Should only be used for debugging. - #[expect(dead_code)] - unsafe fn temp_buf_contents(&self) -> &String { - match &self.temp_bufs[..] { - [] => unsafe { self.buf_contents() }, - [.., buf] => &buf.w, - } - } - - buf_fn! { fn indent(&mut self, delta: usize) } - buf_fn! { fn dedent(&mut self, delta: usize) } - buf_fn! { fn start_group(&mut self) } - buf_fn! { fn end_group(&mut self) } - buf_fn! { fn create_temp_buf(&self) -> FormatBuffer } - buf_fn! { fn restrict_to_single_line(&mut self, restricted: bool) } - buf_fn! { fn current_line_len(&self) -> usize } - buf_fn! { fn total_indent_len(&self) -> usize } - buf_fn! { fn is_beginning_of_line(&self) -> bool } - buf_fn! { fn last_char(&self) -> Option } - buf_fn! { fn last_indent_group_skipped(&self) -> bool } - buf_fn! { fn set_last_indent_group_skipped(&mut self, skip: bool) } - buf_fn! { fn write_raw(&mut self, s: impl AsRef) -> std::fmt::Result } - buf_fn! { fn indent_char(&self) -> char } - - /// Do the callback within the context of a temp buffer - fn with_temp_buf( - &mut self, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result> { - self.temp_bufs.push(self.create_temp_buf()); - let res = fun(self); - let out = self.temp_bufs.pop().unwrap(); - res?; - Ok(out) - } - - /// Does the next written character require whitespace before - fn next_char_needs_space(&self, next_char: char) -> bool { - if self.is_beginning_of_line() { - return false; - } - let last_char = - if let Some(last_char) = self.last_char() { last_char } else { return false }; - if last_char.is_whitespace() || next_char.is_whitespace() { - return false; - } - match last_char { - '{' => match next_char { - '{' | '[' | '(' => false, - '/' => true, - _ => self.config.bracket_spacing, - }, - '(' | '.' | '[' => matches!(next_char, '/'), - '/' => true, - _ => match next_char { - '}' => self.config.bracket_spacing, - ')' | ',' | '.' | ';' | ']' => false, - _ => true, - }, - } - } - - /// Is length of the `text` with respect to already written line <= `config.line_length` - fn will_it_fit(&self, text: impl AsRef) -> bool { - let text = text.as_ref(); - if text.is_empty() { - return true; - } - if text.contains('\n') { - return false; - } - let space: usize = self.next_char_needs_space(text.chars().next().unwrap()).into(); - self.config.line_length - >= self - .total_indent_len() - .saturating_add(self.current_line_len()) - .saturating_add(text.chars().count() + space) - } - - /// Write empty brackets with respect to `config.bracket_spacing` setting: - /// `"{ }"` if `true`, `"{}"` if `false` - fn write_empty_brackets(&mut self) -> Result<()> { - let brackets = if self.config.bracket_spacing { "{ }" } else { "{}" }; - write_chunk!(self, "{brackets}")?; - Ok(()) - } - - /// Write semicolon to the buffer - fn write_semicolon(&mut self) -> Result<()> { - write!(self.buf(), ";")?; - Ok(()) - } - - /// Write whitespace separator to the buffer - /// `"\n"` if `multiline` is `true`, `" "` if `false` - fn write_whitespace_separator(&mut self, multiline: bool) -> Result<()> { - if !self.is_beginning_of_line() { - write!(self.buf(), "{}", if multiline { "\n" } else { " " })?; - } - Ok(()) - } - - /// Write new line with preserved `last_indent_group_skipped` flag - fn write_preserved_line(&mut self) -> Result<()> { - let last_indent_group_skipped = self.last_indent_group_skipped(); - writeln!(self.buf())?; - self.set_last_indent_group_skipped(last_indent_group_skipped); - Ok(()) - } - - /// Write unformatted src and comments for given location. - fn write_raw_src(&mut self, loc: Loc) -> Result<()> { - let disabled_stmts_src = String::from_utf8(self.source.as_bytes()[loc.range()].to_vec()) - .map_err(FormatterError::custom)?; - self.write_raw(disabled_stmts_src.trim_end())?; - self.write_whitespace_separator(true)?; - // Remove comments as they're already included in disabled src. - let _ = self.comments.remove_all_comments_before(loc.end()); - Ok(()) - } - - /// Returns number of blank lines in source between two byte indexes - fn blank_lines(&self, start: usize, end: usize) -> usize { - // because of sorting import statements, start can be greater than end - if start > end { - return 0; - } - self.source[start..end].trim_comments().matches('\n').count() - } - - /// Get the byte offset of the next line - fn find_next_line(&self, byte_offset: usize) -> Option { - let mut iter = self.source[byte_offset..].char_indices(); - while let Some((_, ch)) = iter.next() { - match ch { - '\n' => return iter.next().map(|(idx, _)| byte_offset + idx), - '\r' => { - return iter.next().and_then(|(idx, ch)| match ch { - '\n' => iter.next().map(|(idx, _)| byte_offset + idx), - _ => Some(byte_offset + idx), - }); - } - _ => {} - } - } - None - } - - /// Find the next instance of the character in source excluding comments - fn find_next_in_src(&self, byte_offset: usize, needle: char) -> Option { - self.source[byte_offset..] - .comment_state_char_indices() - .position(|(state, _, ch)| needle == ch && state == CommentState::None) - .map(|p| byte_offset + p) - } - - /// Find the start of the next instance of a slice in source - fn find_next_str_in_src(&self, byte_offset: usize, needle: &str) -> Option { - let subset = &self.source[byte_offset..]; - needle.chars().next().and_then(|first_char| { - subset - .comment_state_char_indices() - .position(|(state, idx, ch)| { - first_char == ch - && state == CommentState::None - && idx + needle.len() <= subset.len() - && subset[idx..idx + needle.len()] == *needle - }) - .map(|p| byte_offset + p) - }) - } - - /// Extends the location to the next instance of a character. Returns true if the loc was - /// extended - fn extend_loc_until(&self, loc: &mut Loc, needle: char) -> bool { - if let Some(end) = self.find_next_in_src(loc.end(), needle).map(|offset| offset + 1) { - *loc = loc.with_end(end); - true - } else { - false - } - } - - /// Return the flag whether the attempt should be made - /// to write the block on a single line. - /// If the block style is configured to [SingleLineBlockStyle::Preserve], - /// lookup whether there was a newline introduced in `[start_from, end_at]` range - /// where `end_at` is the start of the block. - fn should_attempt_block_single_line( - &mut self, - stmt: &mut Statement, - start_from: usize, - ) -> bool { - match self.config.single_line_statement_blocks { - SingleLineBlockStyle::Single => true, - SingleLineBlockStyle::Multi => false, - SingleLineBlockStyle::Preserve => { - let end_at = match stmt { - Statement::Block { statements, .. } if !statements.is_empty() => { - statements.first().as_ref().unwrap().loc().start() - } - Statement::Expression(loc, _) => loc.start(), - _ => stmt.loc().start(), - }; - - self.find_next_line(start_from).is_some_and(|loc| loc >= end_at) - } - } - } - - /// Create a chunk given a string and the location information - fn chunk_at( - &mut self, - byte_offset: usize, - next_byte_offset: Option, - needs_space: Option, - content: impl std::fmt::Display, - ) -> Chunk { - Chunk { - postfixes_before: self.comments.remove_postfixes_before(byte_offset), - prefixes: self.comments.remove_prefixes_before(byte_offset), - content: content.to_string(), - postfixes: next_byte_offset - .map(|byte_offset| self.comments.remove_postfixes_before(byte_offset)) - .unwrap_or_default(), - needs_space, - } - } - - /// Create a chunk given a callback - fn chunked( - &mut self, - byte_offset: usize, - next_byte_offset: Option, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result { - self.chunked_mono(byte_offset, next_byte_offset, &mut fun) - } - - fn chunked_mono( - &mut self, - byte_offset: usize, - next_byte_offset: Option, - fun: &mut dyn FnMut(&mut Self) -> Result<()>, - ) -> Result { - let postfixes_before = self.comments.remove_postfixes_before(byte_offset); - let prefixes = self.comments.remove_prefixes_before(byte_offset); - let content = self.with_temp_buf(fun)?.w; - let postfixes = next_byte_offset - .map(|byte_offset| self.comments.remove_postfixes_before(byte_offset)) - .unwrap_or_default(); - Ok(Chunk { postfixes_before, prefixes, content, postfixes, needs_space: None }) - } - - /// Create a chunk given a [Visitable] item - fn visit_to_chunk( - &mut self, - byte_offset: usize, - next_byte_offset: Option, - visitable: &mut impl Visitable, - ) -> Result { - self.chunked(byte_offset, next_byte_offset, |fmt| { - visitable.visit(fmt)?; - Ok(()) - }) - } - - /// Transform [Visitable] items to the list of chunks - fn items_to_chunks<'b>( - &mut self, - next_byte_offset: Option, - items: impl Iterator + 'b, - ) -> Result> { - let mut items = items.peekable(); - let mut out = Vec::with_capacity(items.size_hint().1.unwrap_or(0)); - while let Some((loc, item)) = items.next() { - let chunk_next_byte_offset = - items.peek().map(|(loc, _)| loc.start()).or(next_byte_offset); - - let chunk = if self.inline_config.is_disabled(loc) { - // If item format is disabled, we determine last disabled line from item and create - // chunk with raw src. - let mut disabled_loc = loc; - self.chunked(disabled_loc.start(), chunk_next_byte_offset, |fmt| { - while fmt.inline_config.is_disabled(disabled_loc) { - if let Some(next_line) = fmt.find_next_line(disabled_loc.end()) { - disabled_loc = disabled_loc.with_end(next_line); - } else { - break; - } - } - fmt.write_raw_src(disabled_loc)?; - Ok(()) - })? - } else { - self.visit_to_chunk(loc.start(), chunk_next_byte_offset, item)? - }; - out.push(chunk); - } - Ok(out) - } - - /// Transform [Visitable] items to a list of chunks and then sort those chunks. - fn items_to_chunks_sorted<'b>( - &mut self, - next_byte_offset: Option, - items: impl Iterator + 'b, - ) -> Result> { - let mut items = items.peekable(); - let mut out = Vec::with_capacity(items.size_hint().1.unwrap_or(0)); - while let Some(item) = items.next() { - let chunk_next_byte_offset = - items.peek().map(|next| next.loc().start()).or(next_byte_offset); - let chunk = self.visit_to_chunk(item.loc().start(), chunk_next_byte_offset, item)?; - out.push((item, chunk)); - } - out.sort_by(|(a, _), (b, _)| a.cmp(b)); - Ok(out.into_iter().map(|(_, c)| c).collect()) - } - - /// Write a comment to the buffer formatted. - /// WARNING: This may introduce a newline if the comment is a Line comment - /// or if the comment are wrapped - fn write_comment(&mut self, comment: &CommentWithMetadata, is_first: bool) -> Result<()> { - if self.inline_config.is_disabled(comment.loc) { - return self.write_raw_comment(comment); - } - - match comment.position { - CommentPosition::Prefix => self.write_prefix_comment(comment, is_first), - CommentPosition::Postfix => self.write_postfix_comment(comment), - } - } - - /// Write a comment with position [CommentPosition::Prefix] - fn write_prefix_comment( - &mut self, - comment: &CommentWithMetadata, - is_first: bool, - ) -> Result<()> { - if !self.is_beginning_of_line() { - self.write_preserved_line()?; - } - if !is_first && comment.has_newline_before { - self.write_preserved_line()?; - } - - if matches!(comment.ty, CommentType::DocBlock) { - let mut lines = comment.contents().trim().lines(); - writeln!(self.buf(), "{}", comment.start_token())?; - lines.try_for_each(|l| self.write_doc_block_line(comment, l))?; - write!(self.buf(), " {}", comment.end_token().unwrap())?; - self.write_preserved_line()?; - return Ok(()); - } - - write!(self.buf(), "{}", comment.start_token())?; - - let mut wrapped = false; - let contents = comment.contents(); - let mut lines = contents.lines().peekable(); - while let Some(line) = lines.next() { - wrapped |= self.write_comment_line(comment, line)?; - if lines.peek().is_some() { - self.write_preserved_line()?; - } - } - - if let Some(end) = comment.end_token() { - // Check if the end token in the original comment was on the separate line - if !wrapped && comment.comment.lines().count() > contents.lines().count() { - self.write_preserved_line()?; - } - write!(self.buf(), "{end}")?; - } - if self.find_next_line(comment.loc.end()).is_some() { - self.write_preserved_line()?; - } - - Ok(()) - } - - /// Write a comment with position [CommentPosition::Postfix] - fn write_postfix_comment(&mut self, comment: &CommentWithMetadata) -> Result<()> { - let indented = self.is_beginning_of_line(); - self.indented_if(indented, 1, |fmt| { - if !indented && fmt.next_char_needs_space('/') { - fmt.write_whitespace_separator(false)?; - } - - write!(fmt.buf(), "{}", comment.start_token())?; - let start_token_pos = fmt.current_line_len(); - - let mut lines = comment.contents().lines().peekable(); - fmt.grouped(|fmt| { - while let Some(line) = lines.next() { - fmt.write_comment_line(comment, line)?; - if lines.peek().is_some() { - fmt.write_whitespace_separator(true)?; - } - } - Ok(()) - })?; - - if let Some(end) = comment.end_token() { - // If comment is not multiline, end token has to be aligned with the start - if fmt.is_beginning_of_line() { - write!(fmt.buf(), "{}{end}", " ".repeat(start_token_pos))?; - } else { - write!(fmt.buf(), "{end}")?; - } - } - - if comment.is_line() { - fmt.write_whitespace_separator(true)?; - } - Ok(()) - }) - } - - /// Write the line of a doc block comment line - fn write_doc_block_line(&mut self, comment: &CommentWithMetadata, line: &str) -> Result<()> { - if line.trim().starts_with('*') { - let line = line.trim().trim_start_matches('*'); - let needs_space = line.chars().next().is_some_and(|ch| !ch.is_whitespace()); - write!(self.buf(), " *{}", if needs_space { " " } else { "" })?; - self.write_comment_line(comment, line)?; - self.write_whitespace_separator(true)?; - return Ok(()); - } - - let indent_whitespace_count = line - .char_indices() - .take_while(|(idx, ch)| ch.is_whitespace() && *idx <= self.buf.current_indent_len()) - .count(); - let to_skip = if indent_whitespace_count < self.buf.current_indent_len() { - 0 - } else { - self.buf.current_indent_len() - }; - - write!(self.buf(), " *")?; - let content = &line[to_skip..]; - if !content.trim().is_empty() { - write!(self.buf(), " ")?; - self.write_comment_line(comment, &line[to_skip..])?; - } - self.write_whitespace_separator(true)?; - Ok(()) - } - - /// Write a comment line that might potentially overflow the maximum line length - /// and, if configured, will be wrapped to the next line. - fn write_comment_line(&mut self, comment: &CommentWithMetadata, line: &str) -> Result { - if self.will_it_fit(line) || !self.config.wrap_comments { - let start_with_ws = - line.chars().next().map(|ch| ch.is_whitespace()).unwrap_or_default(); - if !self.is_beginning_of_line() || !start_with_ws { - write!(self.buf(), "{line}")?; - return Ok(false); - } - - // if this is the beginning of the line, - // the comment should start with at least an indent - let indent = self.buf.current_indent_len(); - let mut chars = line - .char_indices() - .skip_while(|(idx, ch)| ch.is_whitespace() && *idx < indent) - .map(|(_, ch)| ch); - let padded = - format!("{}{}", self.indent_char().to_string().repeat(indent), chars.join("")); - self.write_raw(padded)?; - return Ok(false); - } - - let mut words = line.split(' ').peekable(); - while let Some(word) = words.next() { - if self.is_beginning_of_line() { - write!(self.buf(), "{}", word.trim_start())?; - } else { - self.write_raw(word)?; - } - - if let Some(next) = words.peek() { - if !word.is_empty() && !self.will_it_fit(next) { - // the next word doesn't fit on this line, - // write remaining words on the next - self.write_whitespace_separator(true)?; - // write newline wrap token - write!(self.buf(), "{}", comment.wrap_token())?; - self.write_comment_line(comment, &words.join(" "))?; - return Ok(true); - } - - self.write_whitespace_separator(false)?; - } - } - Ok(false) - } - - /// Write a raw comment. This is like [`write_comment`](Self::write_comment) but won't do any - /// formatting or worry about whitespace behind the comment. - fn write_raw_comment(&mut self, comment: &CommentWithMetadata) -> Result<()> { - self.write_raw(&comment.comment)?; - if comment.is_line() { - self.write_preserved_line()?; - } - Ok(()) - } - - // TODO handle whitespace between comments for disabled sections - /// Write multiple comments - fn write_comments<'b>( - &mut self, - comments: impl IntoIterator, - ) -> Result<()> { - let mut comments = comments.into_iter().peekable(); - let mut last_byte_written = match comments.peek() { - Some(comment) => comment.loc.start(), - None => return Ok(()), - }; - let mut is_first = true; - for comment in comments { - let unwritten_whitespace_loc = - Loc::File(comment.loc.file_no(), last_byte_written, comment.loc.start()); - if self.inline_config.is_disabled(unwritten_whitespace_loc) { - self.write_raw(&self.source[unwritten_whitespace_loc.range()])?; - self.write_raw_comment(comment)?; - last_byte_written = if comment.is_line() { - self.find_next_line(comment.loc.end()).unwrap_or_else(|| comment.loc.end()) - } else { - comment.loc.end() - }; - } else { - self.write_comment(comment, is_first)?; - } - is_first = false; - } - Ok(()) - } - - /// Write a postfix comments before a given location - fn write_postfix_comments_before(&mut self, byte_end: usize) -> Result<()> { - let comments = self.comments.remove_postfixes_before(byte_end); - self.write_comments(&comments) - } - - /// Write all prefix comments before a given location - fn write_prefix_comments_before(&mut self, byte_end: usize) -> Result<()> { - let comments = self.comments.remove_prefixes_before(byte_end); - self.write_comments(&comments) - } - - /// Check if a chunk will fit on the current line - fn will_chunk_fit(&mut self, format_string: &str, chunk: &Chunk) -> Result { - if let Some(chunk_str) = self.simulate_to_single_line(|fmt| fmt.write_chunk(chunk))? { - Ok(self.will_it_fit(format_string.replacen("{}", &chunk_str, 1))) - } else { - Ok(false) - } - } - - /// Check if a separated list of chunks will fit on the current line - fn are_chunks_separated_multiline<'b>( - &mut self, - format_string: &str, - items: impl IntoIterator, - separator: &str, - ) -> Result { - let items = items.into_iter().collect_vec(); - if let Some(chunks) = self.simulate_to_single_line(|fmt| { - fmt.write_chunks_separated(items.iter().copied(), separator, false) - })? { - Ok(!self.will_it_fit(format_string.replacen("{}", &chunks, 1))) - } else { - Ok(true) - } - } - - /// Write the chunk and any surrounding comments into the buffer - /// This will automatically add whitespace before the chunk given the rule set in - /// `next_char_needs_space`. If the chunk does not fit on the current line it will be put on - /// to the next line - fn write_chunk(&mut self, chunk: &Chunk) -> Result<()> { - // handle comments before chunk - self.write_comments(&chunk.postfixes_before)?; - self.write_comments(&chunk.prefixes)?; - - // trim chunk start - let content = if chunk.content.starts_with('\n') { - let mut chunk = chunk.content.trim_start().to_string(); - chunk.insert(0, '\n'); - chunk - } else if chunk.content.starts_with(self.indent_char()) { - let mut chunk = chunk.content.trim_start().to_string(); - chunk.insert(0, ' '); - chunk - } else { - chunk.content.clone() - }; - - if !content.is_empty() { - // add whitespace if necessary - let needs_space = chunk - .needs_space - .unwrap_or_else(|| self.next_char_needs_space(content.chars().next().unwrap())); - if needs_space { - if self.will_it_fit(&content) { - write!(self.buf(), " ")?; - } else { - writeln!(self.buf())?; - } - } - - // write chunk - write!(self.buf(), "{content}")?; - } - - // write any postfix comments - self.write_comments(&chunk.postfixes)?; - - Ok(()) - } - - /// Write chunks separated by a separator. If `multiline`, each chunk will be written to a - /// separate line - fn write_chunks_separated<'b>( - &mut self, - chunks: impl IntoIterator, - separator: &str, - multiline: bool, - ) -> Result<()> { - let mut chunks = chunks.into_iter().peekable(); - while let Some(chunk) = chunks.next() { - let mut chunk = chunk.clone(); - - // handle postfixes before and add newline if necessary - self.write_comments(&std::mem::take(&mut chunk.postfixes_before))?; - if multiline && !self.is_beginning_of_line() { - writeln!(self.buf())?; - } - - // remove postfixes so we can add separator between - let postfixes = std::mem::take(&mut chunk.postfixes); - - self.write_chunk(&chunk)?; - - // add separator - if chunks.peek().is_some() { - write!(self.buf(), "{separator}")?; - self.write_comments(&postfixes)?; - if multiline && !self.is_beginning_of_line() { - writeln!(self.buf())?; - } - } else { - self.write_comments(&postfixes)?; - } - } - Ok(()) - } - - /// Apply the callback indented by the indent size - fn indented(&mut self, delta: usize, fun: impl FnMut(&mut Self) -> Result<()>) -> Result<()> { - self.indented_if(true, delta, fun) - } - - /// Apply the callback indented by the indent size if the condition is true - fn indented_if( - &mut self, - condition: bool, - delta: usize, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result<()> { - if condition { - self.indent(delta); - } - let res = fun(self); - if condition { - self.dedent(delta); - } - res?; - Ok(()) - } - - /// Apply the callback into an indent group. The first line of the indent group is not - /// indented but lines thereafter are - fn grouped(&mut self, mut fun: impl FnMut(&mut Self) -> Result<()>) -> Result { - self.start_group(); - let res = fun(self); - let indented = !self.last_indent_group_skipped(); - self.end_group(); - res?; - Ok(indented) - } - - /// Add a function context around a procedure and revert the context at the end of the procedure - /// regardless of the response - fn with_function_context( - &mut self, - context: FunctionDefinition, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result<()> { - self.context.function = Some(context); - let res = fun(self); - self.context.function = None; - res - } - - /// Add a contract context around a procedure and revert the context at the end of the procedure - /// regardless of the response - fn with_contract_context( - &mut self, - context: ContractDefinition, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result<()> { - self.context.contract = Some(context); - let res = fun(self); - self.context.contract = None; - res - } - - /// Create a transaction. The result of the transaction is not applied to the buffer unless - /// `Transacton::commit` is called - fn transact<'b>( - &'b mut self, - fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result> { - Transaction::new(self, fun) - } - - /// Do the callback and return the result on the buffer as a string - fn simulate_to_string(&mut self, fun: impl FnMut(&mut Self) -> Result<()>) -> Result { - Ok(self.transact(fun)?.buffer) - } - - /// Turn a chunk and its surrounding comments into a string - fn chunk_to_string(&mut self, chunk: &Chunk) -> Result { - self.simulate_to_string(|fmt| fmt.write_chunk(chunk)) - } - - /// Try to create a string based on a callback. If the string does not fit on a single line - /// this will return `None` - fn simulate_to_single_line( - &mut self, - mut fun: impl FnMut(&mut Self) -> Result<()>, - ) -> Result> { - let mut single_line = false; - let tx = self.transact(|fmt| { - fmt.restrict_to_single_line(true); - single_line = match fun(fmt) { - Ok(()) => true, - Err(FormatterError::Fmt(_)) => false, - Err(err) => bail!(err), - }; - Ok(()) - })?; - Ok(if single_line && tx.will_it_fit(&tx.buffer) { Some(tx.buffer) } else { None }) - } - - /// Try to apply a callback to a single line. If the callback cannot be applied to a single - /// line the callback will not be applied to the buffer and `false` will be returned. Otherwise - /// `true` will be returned - fn try_on_single_line(&mut self, mut fun: impl FnMut(&mut Self) -> Result<()>) -> Result { - let mut single_line = false; - let tx = self.transact(|fmt| { - fmt.restrict_to_single_line(true); - single_line = match fun(fmt) { - Ok(()) => true, - Err(FormatterError::Fmt(_)) => false, - Err(err) => bail!(err), - }; - Ok(()) - })?; - Ok(if single_line && tx.will_it_fit(&tx.buffer) { - tx.commit()?; - true - } else { - false - }) - } - - /// Surrounds a callback with parentheses. The callback will try to be applied to a single - /// line. If the callback cannot be applied to a single line the callback will applied to the - /// nextline indented. The callback receives a `multiline` hint as the second argument which - /// receives `true` in the latter case - fn surrounded( - &mut self, - first: SurroundingChunk, - last: SurroundingChunk, - mut fun: impl FnMut(&mut Self, bool) -> Result<()>, - ) -> Result<()> { - let first_chunk = - self.chunk_at(first.loc_before(), first.loc_next(), first.spaced, first.content); - self.write_chunk(&first_chunk)?; - - let multiline = !self.try_on_single_line(|fmt| { - fun(fmt, false)?; - let last_chunk = - fmt.chunk_at(last.loc_before(), last.loc_next(), last.spaced, &last.content); - fmt.write_chunk(&last_chunk)?; - Ok(()) - })?; - - if multiline { - self.indented(1, |fmt| { - fmt.write_whitespace_separator(true)?; - let stringified = fmt.with_temp_buf(|fmt| fun(fmt, true))?.w; - write_chunk!(fmt, "{}", stringified.trim_start()) - })?; - if !last.content.trim_start().is_empty() { - self.indented(1, |fmt| fmt.write_whitespace_separator(true))?; - } - let last_chunk = - self.chunk_at(last.loc_before(), last.loc_next(), last.spaced, &last.content); - self.write_chunk(&last_chunk)?; - } - - Ok(()) - } - - /// Write each [Visitable] item on a separate line. The function will check if there are any - /// blank lines between each visitable statement and will apply a single blank line if there - /// exists any. The `needs_space` callback can force a newline and is given the last_item if - /// any and the next item as arguments - fn write_lined_visitable<'b, I, V, F>( - &mut self, - loc: Loc, - items: I, - needs_space_fn: F, - ) -> Result<()> - where - I: Iterator + 'b, - V: Visitable + CodeLocation + 'b, - F: Fn(&V, &V) -> bool, - { - let mut items = items.collect::>(); - items.reverse(); - // get next item - let pop_next = |fmt: &mut Self, items: &mut Vec<&'b mut V>| { - let comment = - fmt.comments.iter().next().filter(|comment| comment.loc.end() < loc.end()); - let item = items.last(); - if let (Some(comment), Some(item)) = (comment, item) { - if comment.loc < item.loc() { - Some(Either::Left(fmt.comments.pop().unwrap())) - } else { - Some(Either::Right(items.pop().unwrap())) - } - } else if comment.is_some() { - Some(Either::Left(fmt.comments.pop().unwrap())) - } else if item.is_some() { - Some(Either::Right(items.pop().unwrap())) - } else { - None - } - }; - // get whitespace between to offsets. this needs to account for possible left over - // semicolons which are not included in the `Loc` - let unwritten_whitespace = |from: usize, to: usize| { - let to = to.max(from); - let mut loc = Loc::File(loc.file_no(), from, to); - let src = &self.source[from..to]; - if let Some(semi) = src.find(';') { - loc = loc.with_start(from + semi + 1); - } - (loc, &self.source[loc.range()]) - }; - - let mut last_byte_written = match ( - self.comments.iter().next().filter(|comment| comment.loc.end() < loc.end()), - items.last(), - ) { - (Some(comment), Some(item)) => comment.loc.min(item.loc()), - (None, Some(item)) => item.loc(), - (Some(comment), None) => comment.loc, - (None, None) => return Ok(()), - } - .start(); - - let mut last_loc: Option = None; - let mut visited_locs: Vec = Vec::new(); - - // marker for whether the next item needs additional space - let mut needs_space = false; - let mut last_comment = None; - - while let Some(mut line_item) = pop_next(self, &mut items) { - let loc = line_item.as_ref().either(|c| c.loc, |i| i.loc()); - let (unwritten_whitespace_loc, unwritten_whitespace) = - unwritten_whitespace(last_byte_written, loc.start()); - let ignore_whitespace = if self.inline_config.is_disabled(unwritten_whitespace_loc) { - trace!("Unwritten whitespace: {unwritten_whitespace:?}"); - self.write_raw(unwritten_whitespace)?; - true - } else { - false - }; - match line_item.as_mut() { - Either::Left(comment) => { - if ignore_whitespace { - self.write_raw_comment(comment)?; - if unwritten_whitespace.contains('\n') { - needs_space = false; - } - } else { - self.write_comment(comment, last_loc.is_none())?; - if last_loc.is_some() && comment.has_newline_before { - needs_space = false; - } - } - } - Either::Right(item) => { - if !ignore_whitespace { - self.write_whitespace_separator(true)?; - if let Some(mut last_loc) = last_loc { - // here's an edge case when we reordered items so the last_loc isn't - // necessarily the item that directly precedes the current item because - // the order might have changed, so we need to find the last item that - // is before the current item by checking the recorded locations - if let Some(last_item) = visited_locs - .iter() - .rev() - .find(|prev_item| prev_item.start() > last_loc.end()) - { - last_loc = *last_item; - } - - // The blank lines check is susceptible additional trailing new lines - // because the block docs can contain - // multiple lines, but the function def should follow directly after the - // block comment - let is_last_doc_comment = matches!( - last_comment, - Some(CommentWithMetadata { ty: CommentType::DocBlock, .. }) - ); - - if needs_space - || (!is_last_doc_comment - && self.blank_lines(last_loc.end(), loc.start()) > 1) - { - writeln!(self.buf())?; - } - } - } - if let Some(next_item) = items.last() { - needs_space = needs_space_fn(item, next_item); - } - trace!("Visiting {}", { - let n = std::any::type_name::(); - n.strip_prefix("solang_parser::pt::").unwrap_or(n) - }); - item.visit(self)?; - } - } - - last_loc = Some(loc); - visited_locs.push(loc); - - last_comment = None; - - last_byte_written = loc.end(); - if let Some(comment) = line_item.left() { - if comment.is_line() { - last_byte_written = - self.find_next_line(last_byte_written).unwrap_or(last_byte_written); - } - last_comment = Some(comment); - } - } - - // write manually to avoid eof comment being detected as first - let comments = self.comments.remove_prefixes_before(loc.end()); - for comment in comments { - self.write_comment(&comment, false)?; - } - - let (unwritten_src_loc, mut unwritten_whitespace) = - unwritten_whitespace(last_byte_written, loc.end()); - if self.inline_config.is_disabled(unwritten_src_loc) { - if unwritten_src_loc.end() == self.source.len() { - // remove EOF line ending - unwritten_whitespace = unwritten_whitespace - .strip_suffix('\n') - .map(|w| w.strip_suffix('\r').unwrap_or(w)) - .unwrap_or(unwritten_whitespace); - } - trace!("Unwritten whitespace: {unwritten_whitespace:?}"); - self.write_raw(unwritten_whitespace)?; - } - - Ok(()) - } - - /// Visit the right side of an assignment. The function will try to write the assignment on a - /// single line or indented on the next line. If it can't do this it resorts to letting the - /// expression decide how to split itself on multiple lines - fn visit_assignment(&mut self, expr: &mut Expression) -> Result<()> { - if self.try_on_single_line(|fmt| expr.visit(fmt))? { - return Ok(()); - } - - self.write_postfix_comments_before(expr.loc().start())?; - self.write_prefix_comments_before(expr.loc().start())?; - - if self.try_on_single_line(|fmt| fmt.indented(1, |fmt| expr.visit(fmt)))? { - return Ok(()); - } - - let mut fit_on_next_line = false; - self.indented(1, |fmt| { - let tx = fmt.transact(|fmt| { - writeln!(fmt.buf())?; - fit_on_next_line = fmt.try_on_single_line(|fmt| expr.visit(fmt))?; - Ok(()) - })?; - if fit_on_next_line { - tx.commit()?; - } - Ok(()) - })?; - - if !fit_on_next_line { - self.indented_if(expr.is_unsplittable(), 1, |fmt| expr.visit(fmt))?; - } - - Ok(()) - } - - /// Visit the list of comma separated items. - /// If the prefix is not empty, then the function will write - /// the whitespace before the parentheses (if they are required). - fn visit_list( - &mut self, - prefix: &str, - items: &mut [T], - start_offset: Option, - end_offset: Option, - paren_required: bool, - ) -> Result<()> - where - T: Visitable + CodeLocation, - { - write_chunk!(self, "{}", prefix)?; - let whitespace = if !prefix.is_empty() { " " } else { "" }; - let next_after_start_offset = items.first().map(|item| item.loc().start()); - let first_surrounding = SurroundingChunk::new("", start_offset, next_after_start_offset); - let last_surrounding = SurroundingChunk::new(")", None, end_offset); - if items.is_empty() { - if paren_required { - write!(self.buf(), "{whitespace}(")?; - self.surrounded(first_surrounding, last_surrounding, |fmt, _| { - // write comments before the list end - write_chunk!(fmt, end_offset.unwrap_or_default(), "")?; - Ok(()) - })?; - } - } else { - write!(self.buf(), "{whitespace}(")?; - self.surrounded(first_surrounding, last_surrounding, |fmt, multiline| { - let args = - fmt.items_to_chunks(end_offset, items.iter_mut().map(|arg| (arg.loc(), arg)))?; - let multiline = - multiline && fmt.are_chunks_separated_multiline("{}", &args, ",")?; - fmt.write_chunks_separated(&args, ",", multiline)?; - Ok(()) - })?; - } - Ok(()) - } - - /// Visit the block item. Attempt to write it on the single - /// line if requested. Surround by curly braces and indent - /// each line otherwise. Returns `true` if the block fit - /// on a single line - fn visit_block( - &mut self, - loc: Loc, - statements: &mut [T], - attempt_single_line: bool, - attempt_omit_braces: bool, - ) -> Result - where - T: Visitable + CodeLocation, - { - if attempt_single_line && statements.len() == 1 { - let fits_on_single = self.try_on_single_line(|fmt| { - if !attempt_omit_braces { - write!(fmt.buf(), "{{ ")?; - } - statements.first_mut().unwrap().visit(fmt)?; - if !attempt_omit_braces { - write!(fmt.buf(), " }}")?; - } - Ok(()) - })?; - - if fits_on_single { - return Ok(true); - } - } - - // Determine if any of start / end of the block is disabled and block lines boundaries. - let is_start_disabled = self.inline_config.is_disabled(loc.with_end(loc.start())); - let is_end_disabled = self.inline_config.is_disabled(loc.with_start(loc.end())); - let end_of_first_line = self.find_next_line(loc.start()).unwrap_or_default(); - let end_of_last_line = self.find_next_line(loc.end()).unwrap_or_default(); - - // Write first line of the block: - // - as it is until the end of line, if format disabled - // - start block if line formatted - if is_start_disabled { - self.write_raw_src(loc.with_end(end_of_first_line))?; - } else { - write_chunk!(self, "{{")?; - } - - // Write comments and close block if no statement. - if statements.is_empty() { - self.indented(1, |fmt| { - fmt.write_prefix_comments_before(loc.end())?; - fmt.write_postfix_comments_before(loc.end())?; - Ok(()) - })?; - - write_chunk!(self, "}}")?; - return Ok(false); - } - - // Determine writable statements by excluding statements from disabled start / end lines. - // We check the position of last statement from first line (if disabled) and position of - // first statement from last line (if disabled) and slice accordingly. - let writable_statements = match ( - statements.iter().rposition(|stmt| { - is_start_disabled - && self.find_next_line(stmt.loc().end()).unwrap_or_default() - == end_of_first_line - }), - statements.iter().position(|stmt| { - is_end_disabled - && self.find_next_line(stmt.loc().end()).unwrap_or_default() == end_of_last_line - }), - ) { - // We have statements on both disabled start / end lines. - (Some(start), Some(end)) => { - if start == end || start + 1 == end { - None - } else { - Some(&mut statements[start + 1..end]) - } - } - // We have statements only on disabled start line. - (Some(start), None) => { - if start + 1 == statements.len() { - None - } else { - Some(&mut statements[start + 1..]) - } - } - // We have statements only on disabled end line. - (None, Some(end)) => { - if end == 0 { - None - } else { - Some(&mut statements[..end]) - } - } - // No statements on disabled start / end line. - (None, None) => Some(statements), - }; - - // Write statements that are not on any disabled first / last block line. - let mut statements_loc = loc; - if let Some(writable_statements) = writable_statements { - if let Some(first_statement) = writable_statements.first() { - statements_loc = statements_loc.with_start(first_statement.loc().start()); - self.write_whitespace_separator(true)?; - self.write_postfix_comments_before(statements_loc.start())?; - } - // If last line is disabled then statements location ends where last block line starts. - if is_end_disabled && let Some(last_statement) = writable_statements.last() { - statements_loc = statements_loc - .with_end(self.find_next_line(last_statement.loc().end()).unwrap_or_default()); - } - self.indented(1, |fmt| { - fmt.write_lined_visitable( - statements_loc, - writable_statements.iter_mut(), - |_, _| false, - )?; - Ok(()) - })?; - self.write_whitespace_separator(true)?; - } - - // Write last line of the block: - // - as it is from where statements location ends until the end of last line, if format - // disabled - // - close block if line formatted - if is_end_disabled { - self.write_raw_src(loc.with_start(statements_loc.end()).with_end(end_of_last_line))?; - } else { - if end_of_first_line != end_of_last_line { - self.write_whitespace_separator(true)?; - } - write_chunk!(self, loc.end(), "}}")?; - } - - Ok(false) - } - - /// Visit statement as `Statement::Block`. - fn visit_stmt_as_block( - &mut self, - stmt: &mut Statement, - attempt_single_line: bool, - ) -> Result { - match stmt { - Statement::Block { loc, statements, .. } => { - self.visit_block(*loc, statements, attempt_single_line, true) - } - _ => self.visit_block(stmt.loc(), &mut [stmt], attempt_single_line, true), - } - } - - /// Visit the generic member access expression and - /// attempt flatten it by checking if the inner expression - /// matches a given member access variant. - fn visit_member_access<'b, T, M>( - &mut self, - expr: &'b mut Box, - ident: &mut Identifier, - mut matcher: M, - ) -> Result<()> - where - T: CodeLocation + Visitable, - M: FnMut(&mut Self, &'b mut Box) -> Result, &'b mut Identifier)>>, - { - let chunk_member_access = |fmt: &mut Self, ident: &mut Identifier, expr: &mut Box| { - fmt.chunked(ident.loc.start(), Some(expr.loc().start()), |fmt| ident.visit(fmt)) - }; - - let mut chunks: Vec = vec![chunk_member_access(self, ident, expr)?]; - let mut remaining = expr; - while let Some((inner_expr, inner_ident)) = matcher(self, remaining)? { - chunks.push(chunk_member_access(self, inner_ident, inner_expr)?); - remaining = inner_expr; - } - - chunks.reverse(); - chunks.iter_mut().for_each(|chunk| chunk.content.insert(0, '.')); - - if !self.try_on_single_line(|fmt| fmt.write_chunks_separated(&chunks, "", false))? { - self.grouped(|fmt| fmt.write_chunks_separated(&chunks, "", true))?; - } - Ok(()) - } - - /// Visit the yul string with an optional identifier. - /// If the identifier is present, write the value in the format `:`. - /// - /// Ref: - fn visit_yul_string_with_ident( - &mut self, - loc: Loc, - val: &str, - ident: &mut Option, - ) -> Result<()> { - let ident = - if let Some(ident) = ident { format!(":{}", ident.name) } else { String::new() }; - write_chunk!(self, loc.start(), loc.end(), "{val}{ident}")?; - Ok(()) - } - - /// Format a quoted string as `prefix"string"` where the quote character is handled - /// by the configuration `quote_style` - fn quote_str(&self, loc: Loc, prefix: Option<&str>, string: &str) -> String { - let get_og_quote = || { - self.source[loc.range()] - .quote_state_char_indices() - .find_map( - |(state, _, ch)| { - if matches!(state, QuoteState::Opening(_)) { Some(ch) } else { None } - }, - ) - .expect("Could not find quote character for quoted string") - }; - let mut quote = self.config.quote_style.quote().unwrap_or_else(get_og_quote); - let mut quoted = format!("{quote}{string}{quote}"); - if !quoted.is_quoted() { - quote = get_og_quote(); - quoted = format!("{quote}{string}{quote}"); - } - let prefix = prefix.unwrap_or(""); - format!("{prefix}{quoted}") - } - - /// Write a quoted string. See `Formatter::quote_str` for more information - fn write_quoted_str(&mut self, loc: Loc, prefix: Option<&str>, string: &str) -> Result<()> { - write_chunk!(self, loc.start(), loc.end(), "{}", self.quote_str(loc, prefix, string)) - } - - /// Write and format numbers. This will fix underscores as well as remove unnecessary 0's and - /// exponents - fn write_num_literal( - &mut self, - loc: Loc, - value: &str, - fractional: Option<&str>, - exponent: &str, - unit: &mut Option, - ) -> Result<()> { - let config = self.config.number_underscore; - - // get source if we preserve underscores - let (value, fractional, exponent) = if config.is_preserve() { - let source = &self.source[loc.start()..loc.end()]; - // Strip unit - let (source, _) = source.split_once(' ').unwrap_or((source, "")); - let (val, exp) = source.split_once(['e', 'E']).unwrap_or((source, "")); - let (val, fract) = - val.split_once('.').map(|(val, fract)| (val, Some(fract))).unwrap_or((val, None)); - ( - val.trim().to_string(), - fract.map(|fract| fract.trim().to_string()), - exp.trim().to_string(), - ) - } else { - // otherwise strip underscores - ( - value.trim().replace('_', ""), - fractional.map(|fract| fract.trim().replace('_', "")), - exponent.trim().replace('_', ""), - ) - }; - - // strip any padded 0's - let val = value.trim_start_matches('0'); - let fract = fractional.as_ref().map(|fract| fract.trim_end_matches('0')); - let (exp_sign, mut exp) = if let Some(exp) = exponent.strip_prefix('-') { - ("-", exp) - } else { - ("", exponent.as_str()) - }; - exp = exp.trim().trim_start_matches('0'); - - let add_underscores = |string: &str, reversed: bool| -> String { - if !config.is_thousands() || string.len() < 5 { - return string.to_string(); - } - if reversed { - Box::new(string.as_bytes().chunks(3)) as Box> - } else { - Box::new(string.as_bytes().rchunks(3).rev()) as Box> - } - .map(|chunk| std::str::from_utf8(chunk).expect("valid utf8 content.")) - .collect::>() - .join("_") - }; - - let mut out = String::new(); - if val.is_empty() { - out.push('0'); - } else { - out.push_str(&add_underscores(val, false)); - } - if let Some(fract) = fract { - out.push('.'); - if fract.is_empty() { - out.push('0'); - } else { - // TODO re-enable me on the next solang-parser v0.1.18 - // currently disabled because of the following bug - // https://github.com/hyperledger-labs/solang/pull/954 - // out.push_str(&add_underscores(fract, true)); - out.push_str(fract) - } - } - if !exp.is_empty() { - out.push('e'); - out.push_str(exp_sign); - out.push_str(&add_underscores(exp, false)); - } - - write_chunk!(self, loc.start(), loc.end(), "{out}")?; - self.write_unit(unit) - } - - /// Write and hex literals according to the configuration. - fn write_hex_literal(&mut self, lit: &HexLiteral) -> Result<()> { - let HexLiteral { loc, hex } = lit; - match self.config.hex_underscore { - HexUnderscore::Remove => self.write_quoted_str(*loc, Some("hex"), hex), - HexUnderscore::Preserve => { - let quote = &self.source[loc.start()..loc.end()].trim_start_matches("hex"); - // source is always quoted so we remove the quotes first so we can adhere to the - // configured quoting style - let hex = "e[1..quote.len() - 1]; - self.write_quoted_str(*loc, Some("hex"), hex) - } - HexUnderscore::Bytes => { - // split all bytes - let hex = hex - .chars() - .chunks(2) - .into_iter() - .map(|chunk| chunk.collect::()) - .collect::>() - .join("_"); - self.write_quoted_str(*loc, Some("hex"), &hex) - } - } - } - - /// Write built-in unit. - fn write_unit(&mut self, unit: &mut Option) -> Result<()> { - if let Some(unit) = unit { - write_chunk!(self, unit.loc.start(), unit.loc.end(), "{}", unit.name)?; - } - Ok(()) - } - - /// Write the function header - fn write_function_header( - &mut self, - func: &mut FunctionDefinition, - body_loc: Option, - header_multiline: bool, - ) -> Result { - let func_name = if let Some(ident) = &func.name { - format!("{} {}", func.ty, ident.name) - } else { - func.ty.to_string() - }; - - // calculate locations of chunk groups - let attrs_loc = func.attributes.first().map(|attr| attr.loc()); - let returns_loc = func.returns.first().map(|param| param.0); - - let params_next_offset = attrs_loc - .as_ref() - .or(returns_loc.as_ref()) - .or(body_loc.as_ref()) - .map(|loc| loc.start()); - let attrs_end = returns_loc.as_ref().or(body_loc.as_ref()).map(|loc| loc.start()); - let returns_end = body_loc.as_ref().map(|loc| loc.start()); - - let mut params_multiline = false; - - let params_loc = { - let mut loc = func.loc.with_end(func.loc.start()); - self.extend_loc_until(&mut loc, ')'); - loc - }; - let params_disabled = self.inline_config.is_disabled(params_loc); - if params_disabled { - let chunk = self.chunked(func.loc.start(), None, |fmt| fmt.visit_source(params_loc))?; - params_multiline = chunk.content.contains('\n'); - self.write_chunk(&chunk)?; - } else { - let first_surrounding = SurroundingChunk::new( - format!("{func_name}("), - Some(func.loc.start()), - Some( - func.params - .first() - .map(|param| param.0.start()) - .unwrap_or_else(|| params_loc.end()), - ), - ); - self.surrounded( - first_surrounding, - SurroundingChunk::new(")", None, params_next_offset), - |fmt, multiline| { - let params = fmt.items_to_chunks( - params_next_offset, - func.params - .iter_mut() - .filter_map(|(loc, param)| param.as_mut().map(|param| (*loc, param))), - )?; - let after_params = if !func.attributes.is_empty() || !func.returns.is_empty() { - "" - } else if func.body.is_some() { - " {" - } else { - ";" - }; - let should_multiline = header_multiline - && matches!( - fmt.config.multiline_func_header, - MultilineFuncHeaderStyle::ParamsFirst - | MultilineFuncHeaderStyle::ParamsFirstMulti - | MultilineFuncHeaderStyle::All - | MultilineFuncHeaderStyle::AllParams - ); - params_multiline = should_multiline - || multiline - || fmt.are_chunks_separated_multiline( - &format!("{{}}){after_params}"), - ¶ms, - ",", - )?; - // Write new line if we have only one parameter and params first set, - // or if the function definition is multiline and all params set. - let single_param_multiline = matches!( - fmt.config.multiline_func_header, - MultilineFuncHeaderStyle::ParamsFirst - ) || params_multiline - && matches!( - fmt.config.multiline_func_header, - MultilineFuncHeaderStyle::AllParams - ); - if params.len() == 1 && single_param_multiline { - writeln!(fmt.buf())?; - } - fmt.write_chunks_separated(¶ms, ",", params_multiline)?; - Ok(()) - }, - )?; - } - - let mut write_attributes = |fmt: &mut Self, multiline: bool| -> Result<()> { - // write attributes - if !func.attributes.is_empty() { - let attrs_loc = func - .attributes - .first() - .unwrap() - .loc() - .with_end_from(&func.attributes.last().unwrap().loc()); - if fmt.inline_config.is_disabled(attrs_loc) { - // If params are also disabled then write functions attributes on the same line. - if params_disabled { - fmt.write_whitespace_separator(false)?; - let attrs_src = - String::from_utf8(self.source.as_bytes()[attrs_loc.range()].to_vec()) - .map_err(FormatterError::custom)?; - fmt.write_raw(attrs_src)?; - } else { - fmt.indented(1, |fmt| fmt.visit_source(attrs_loc))?; - } - } else { - fmt.write_postfix_comments_before(attrs_loc.start())?; - fmt.write_whitespace_separator(multiline)?; - let attributes = - fmt.items_to_chunks_sorted(attrs_end, func.attributes.iter_mut())?; - fmt.indented(1, |fmt| { - fmt.write_chunks_separated(&attributes, "", multiline)?; - Ok(()) - })?; - } - } - - // write returns - if !func.returns.is_empty() { - let returns_start_loc = func.returns.first().unwrap().0; - let returns_loc = returns_start_loc.with_end_from(&func.returns.last().unwrap().0); - if fmt.inline_config.is_disabled(returns_loc) { - fmt.write_whitespace_separator(false)?; - let returns_src = - String::from_utf8(self.source.as_bytes()[returns_loc.range()].to_vec()) - .map_err(FormatterError::custom)?; - fmt.write_raw(format!("returns ({returns_src})"))?; - } else { - let mut returns = fmt.items_to_chunks( - returns_end, - func.returns - .iter_mut() - .filter_map(|(loc, param)| param.as_mut().map(|param| (*loc, param))), - )?; - - // there's an issue with function return value that would lead to indent issues because those can be formatted with line breaks - for function_chunk in - returns.iter_mut().filter(|chunk| chunk.content.starts_with("function(")) - { - // this will bypass the recursive indent that was applied when the function - // content was formatted in the chunk - function_chunk.content = function_chunk - .content - .split('\n') - .map(|s| s.trim_start()) - .collect::>() - .join("\n"); - } - - fmt.write_postfix_comments_before(returns_loc.start())?; - fmt.write_whitespace_separator(multiline)?; - fmt.indented(1, |fmt| { - fmt.surrounded( - SurroundingChunk::new("returns (", Some(returns_loc.start()), None), - SurroundingChunk::new(")", None, returns_end), - |fmt, multiline_hint| { - fmt.write_chunks_separated(&returns, ",", multiline_hint)?; - Ok(()) - }, - )?; - Ok(()) - })?; - } - } - Ok(()) - }; - - let should_multiline = header_multiline - && if params_multiline { - matches!( - self.config.multiline_func_header, - MultilineFuncHeaderStyle::All | MultilineFuncHeaderStyle::AllParams - ) - } else { - matches!( - self.config.multiline_func_header, - MultilineFuncHeaderStyle::AttributesFirst - ) - }; - let attrs_multiline = should_multiline - || !self.try_on_single_line(|fmt| { - write_attributes(fmt, false)?; - if !fmt.will_it_fit(if func.body.is_some() { " {" } else { ";" }) { - bail!(FormatterError::fmt()) - } - Ok(()) - })?; - if attrs_multiline { - write_attributes(self, true)?; - } - Ok(attrs_multiline) - } - - /// Write potentially nested `if statements` - fn write_if_stmt( - &mut self, - loc: Loc, - cond: &mut Expression, - if_branch: &mut Box, - else_branch: &mut Option>, - ) -> Result<(), FormatterError> { - let single_line_stmt_wide = self.context.if_stmt_single_line.unwrap_or_default(); - - visit_source_if_disabled_else!(self, loc.with_end(if_branch.loc().start()), { - self.surrounded( - SurroundingChunk::new("if (", Some(loc.start()), Some(cond.loc().start())), - SurroundingChunk::new(")", None, Some(if_branch.loc().start())), - |fmt, _| { - fmt.write_prefix_comments_before(cond.loc().end())?; - cond.visit(fmt)?; - fmt.write_postfix_comments_before(if_branch.loc().start()) - }, - )?; - }); - - let cond_close_paren_loc = - self.find_next_in_src(cond.loc().end(), ')').unwrap_or_else(|| cond.loc().end()); - let attempt_single_line = single_line_stmt_wide - && self.should_attempt_block_single_line(if_branch.as_mut(), cond_close_paren_loc); - let if_branch_is_single_line = self.visit_stmt_as_block(if_branch, attempt_single_line)?; - if single_line_stmt_wide && !if_branch_is_single_line { - bail!(FormatterError::fmt()) - } - - if let Some(else_branch) = else_branch { - self.write_postfix_comments_before(else_branch.loc().start())?; - if if_branch_is_single_line { - writeln!(self.buf())?; - } - write_chunk!(self, else_branch.loc().start(), "else")?; - if let Statement::If(loc, cond, if_branch, else_branch) = else_branch.as_mut() { - self.visit_if(*loc, cond, if_branch, else_branch, false)?; - } else { - let else_branch_is_single_line = - self.visit_stmt_as_block(else_branch, attempt_single_line)?; - if single_line_stmt_wide && !else_branch_is_single_line { - bail!(FormatterError::fmt()) - } - } - } - Ok(()) - } - - /// Sorts grouped import statement alphabetically. - fn sort_imports(&self, source_unit: &mut SourceUnit) { - // first we need to find the grouped import statements - // A group is defined as a set of import statements that are separated by a blank line - let mut import_groups = Vec::new(); - let mut current_group = Vec::new(); - let mut source_unit_parts = source_unit.0.iter().enumerate().peekable(); - while let Some((i, part)) = source_unit_parts.next() { - if let SourceUnitPart::ImportDirective(_) = part { - current_group.push(i); - let current_loc = part.loc(); - if let Some((_, next_part)) = source_unit_parts.peek() { - let next_loc = next_part.loc(); - // import statements are followed by a new line, so if there are more than one - // we have a group - if self.blank_lines(current_loc.end(), next_loc.start()) > 1 { - import_groups.push(std::mem::take(&mut current_group)); - } - } - } else if !current_group.is_empty() { - import_groups.push(std::mem::take(&mut current_group)); - } - } - - if !current_group.is_empty() { - import_groups.push(current_group); - } - - if import_groups.is_empty() { - // nothing to sort - return; - } - - // order all groups alphabetically - for group in &import_groups { - // SAFETY: group is not empty - let first = group[0]; - let last = group.last().copied().expect("group is not empty"); - let import_directives = &mut source_unit.0[first..=last]; - - // sort rename style imports alphabetically based on the actual import and not the - // rename - for source_unit_part in import_directives.iter_mut() { - if let SourceUnitPart::ImportDirective(Import::Rename(_, renames, _)) = - source_unit_part - { - renames.sort_by_cached_key(|(og_ident, _)| og_ident.name.clone()); - } - } - - import_directives.sort_by_cached_key(|item| match item { - SourceUnitPart::ImportDirective(import) => match import { - Import::Plain(path, _) => path.to_string(), - Import::GlobalSymbol(path, _, _) => path.to_string(), - Import::Rename(path, _, _) => path.to_string(), - }, - _ => { - unreachable!("import group contains non-import statement") - } - }); - } - } -} - -// Traverse the Solidity Parse Tree and write to the code formatter -impl Visitor for Formatter<'_, W> { - type Error = FormatterError; - - #[instrument(name = "source", skip(self))] - fn visit_source(&mut self, loc: Loc) -> Result<()> { - let source = String::from_utf8(self.source.as_bytes()[loc.range()].to_vec()) - .map_err(FormatterError::custom)?; - let mut lines = source.splitn(2, '\n'); - - write_chunk!(self, loc.start(), "{}", lines.next().unwrap())?; - if let Some(remainder) = lines.next() { - // Call with `self.write_str` and not `write!`, so we can have `\n` at the beginning - // without triggering an indentation - self.write_raw(format!("\n{remainder}"))?; - } - - let _ = self.comments.remove_all_comments_before(loc.end()); - - Ok(()) - } - - #[instrument(name = "SU", skip_all)] - fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { - if self.config.sort_imports { - self.sort_imports(source_unit); - } - // TODO: do we need to put pragma and import directives at the top of the file? - // source_unit.0.sort_by_key(|item| match item { - // SourceUnitPart::PragmaDirective(_, _, _) => 0, - // SourceUnitPart::ImportDirective(_, _) => 1, - // _ => usize::MAX, - // }); - let loc = Loc::File( - source_unit - .loc_opt() - .or_else(|| self.comments.iter().next().map(|comment| comment.loc)) - .map(|loc| loc.file_no()) - .unwrap_or_default(), - 0, - self.source.len(), - ); - - self.write_lined_visitable( - loc, - source_unit.0.iter_mut(), - |last_unit, unit| match last_unit { - SourceUnitPart::PragmaDirective(..) => { - !matches!(unit, SourceUnitPart::PragmaDirective(..)) - } - SourceUnitPart::ImportDirective(_) => { - !matches!(unit, SourceUnitPart::ImportDirective(_)) - } - SourceUnitPart::ErrorDefinition(_) => { - !matches!(unit, SourceUnitPart::ErrorDefinition(_)) - } - SourceUnitPart::Using(_) => !matches!(unit, SourceUnitPart::Using(_)), - SourceUnitPart::VariableDefinition(_) => { - !matches!(unit, SourceUnitPart::VariableDefinition(_)) - } - SourceUnitPart::Annotation(_) => false, - _ => true, - }, - )?; - - // EOF newline - if self.last_char() != Some('\n') { - writeln!(self.buf())?; - } - - Ok(()) - } - - #[instrument(name = "contract", skip_all)] - fn visit_contract(&mut self, contract: &mut ContractDefinition) -> Result<()> { - return_source_if_disabled!(self, contract.loc); - - self.with_contract_context(contract.clone(), |fmt| { - let contract_name = contract.name.safe_unwrap(); - - visit_source_if_disabled_else!( - fmt, - contract.loc.with_end_from( - &contract.base.first().map(|b| b.loc).unwrap_or(contract_name.loc) - ), - { - fmt.grouped(|fmt| { - write_chunk!(fmt, contract.loc.start(), "{}", contract.ty)?; - write_chunk!(fmt, contract_name.loc.end(), "{}", contract_name.name)?; - if !contract.base.is_empty() { - write_chunk!( - fmt, - contract_name.loc.end(), - contract.base.first().unwrap().loc.start(), - "is" - )?; - } - Ok(()) - })?; - } - ); - - if !contract.base.is_empty() { - visit_source_if_disabled_else!( - fmt, - contract - .base - .first() - .unwrap() - .loc - .with_end_from(&contract.base.last().unwrap().loc), - { - fmt.indented(1, |fmt| { - let base_end = contract.parts.first().map(|part| part.loc().start()); - let bases = fmt.items_to_chunks( - base_end, - contract.base.iter_mut().map(|base| (base.loc, base)), - )?; - let multiline = - fmt.are_chunks_separated_multiline("{}", &bases, ",")?; - fmt.write_chunks_separated(&bases, ",", multiline)?; - fmt.write_whitespace_separator(multiline)?; - Ok(()) - })?; - } - ); - } - - if let Some(layout) = &mut contract.layout { - write_chunk!(fmt, "layout at ")?; - fmt.visit_expr(layout.loc(), layout)?; - write_chunk!(fmt, " ")?; - } - - write_chunk!(fmt, "{{")?; - - fmt.indented(1, |fmt| { - if let Some(first) = contract.parts.first() { - fmt.write_postfix_comments_before(first.loc().start())?; - fmt.write_whitespace_separator(true)?; - } else { - return Ok(()); - } - - if fmt.config.contract_new_lines { - write_chunk!(fmt, "\n")?; - } - - fmt.write_lined_visitable( - contract.loc, - contract.parts.iter_mut(), - |last_part, part| match last_part { - ContractPart::ErrorDefinition(_) => { - !matches!(part, ContractPart::ErrorDefinition(_)) - } - ContractPart::EventDefinition(_) => { - !matches!(part, ContractPart::EventDefinition(_)) - } - ContractPart::VariableDefinition(_) => { - !matches!(part, ContractPart::VariableDefinition(_)) - } - ContractPart::TypeDefinition(_) => { - !matches!(part, ContractPart::TypeDefinition(_)) - } - ContractPart::EnumDefinition(_) => { - !matches!(part, ContractPart::EnumDefinition(_)) - } - ContractPart::Using(_) => !matches!(part, ContractPart::Using(_)), - ContractPart::FunctionDefinition(last_def) => { - if last_def.is_empty() { - match part { - ContractPart::FunctionDefinition(def) => !def.is_empty(), - _ => true, - } - } else { - true - } - } - ContractPart::Annotation(_) => false, - _ => true, - }, - ) - })?; - - if !contract.parts.is_empty() { - fmt.write_whitespace_separator(true)?; - - if fmt.config.contract_new_lines { - write_chunk!(fmt, "\n")?; - } - } - - write_chunk!(fmt, contract.loc.end(), "}}")?; - - Ok(()) - })?; - - Ok(()) - } - - // Support extension for Solana/Substrate - #[instrument(name = "annotation", skip_all)] - fn visit_annotation(&mut self, annotation: &mut Annotation) -> Result<()> { - return_source_if_disabled!(self, annotation.loc); - let id = self.simulate_to_string(|fmt| annotation.id.visit(fmt))?; - write!(self.buf(), "@{id}")?; - write!(self.buf(), "(")?; - annotation.value.visit(self)?; - write!(self.buf(), ")")?; - Ok(()) - } - - fn visit_pragma( - &mut self, - pragma: &mut PragmaDirective, - ) -> std::result::Result<(), Self::Error> { - let loc = pragma.loc(); - return_source_if_disabled!(self, loc, ';'); - - match pragma { - PragmaDirective::Identifier(loc, id1, id2) => { - write_chunk!( - self, - loc.start(), - loc.end(), - "pragma {}{}{};", - id1.as_ref().map(|id| id.name.to_string()).unwrap_or_default(), - if id1.is_some() && id2.is_some() { " " } else { "" }, - id2.as_ref().map(|id| id.name.to_string()).unwrap_or_default(), - )?; - } - PragmaDirective::StringLiteral(_loc, id, lit) => { - write_chunk!(self, "pragma {} ", id.name)?; - let StringLiteral { loc, string, .. } = lit; - write_chunk!(self, loc.start(), loc.end(), "\"{string}\";")?; - } - PragmaDirective::Version(loc, id, version) => { - write_chunk!(self, loc.start(), id.loc().end(), "pragma {}", id.name)?; - let version_loc = loc.with_start(version[0].loc().start()); - self.visit_source(version_loc)?; - self.write_semicolon()?; - } - } - Ok(()) - } - - #[instrument(name = "import_plain", skip_all)] - fn visit_import_plain(&mut self, loc: Loc, import: &mut ImportPath) -> Result<()> { - return_source_if_disabled!(self, loc, ';'); - - self.grouped(|fmt| { - write_chunk!(fmt, loc.start(), import.loc().start(), "import")?; - fmt.write_quoted_str(import.loc(), None, &import_path_string(import))?; - fmt.write_semicolon()?; - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "import_global", skip_all)] - fn visit_import_global( - &mut self, - loc: Loc, - global: &mut ImportPath, - alias: &mut Identifier, - ) -> Result<()> { - return_source_if_disabled!(self, loc, ';'); - - self.grouped(|fmt| { - write_chunk!(fmt, loc.start(), global.loc().start(), "import")?; - fmt.write_quoted_str(global.loc(), None, &import_path_string(global))?; - write_chunk!(fmt, loc.start(), alias.loc.start(), "as")?; - alias.visit(fmt)?; - fmt.write_semicolon()?; - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "import_renames", skip_all)] - fn visit_import_renames( - &mut self, - loc: Loc, - imports: &mut [(Identifier, Option)], - from: &mut ImportPath, - ) -> Result<()> { - return_source_if_disabled!(self, loc, ';'); - - if imports.is_empty() { - self.grouped(|fmt| { - write_chunk!(fmt, loc.start(), "import")?; - fmt.write_empty_brackets()?; - write_chunk!(fmt, loc.start(), from.loc().start(), "from")?; - fmt.write_quoted_str(from.loc(), None, &import_path_string(from))?; - fmt.write_semicolon()?; - Ok(()) - })?; - return Ok(()); - } - - let imports_start = imports.first().unwrap().0.loc.start(); - - write_chunk!(self, loc.start(), imports_start, "import")?; - - self.surrounded( - SurroundingChunk::new("{", Some(imports_start), None), - SurroundingChunk::new("}", None, Some(from.loc().start())), - |fmt, _multiline| { - let mut imports = imports.iter_mut().peekable(); - let mut import_chunks = Vec::new(); - while let Some((ident, alias)) = imports.next() { - import_chunks.push(fmt.chunked( - ident.loc.start(), - imports.peek().map(|(ident, _)| ident.loc.start()), - |fmt| { - fmt.grouped(|fmt| { - ident.visit(fmt)?; - if let Some(alias) = alias { - write_chunk!(fmt, ident.loc.end(), alias.loc.start(), "as")?; - alias.visit(fmt)?; - } - Ok(()) - })?; - Ok(()) - }, - )?); - } - - let multiline = fmt.are_chunks_separated_multiline( - &format!("{{}} }} from \"{}\";", import_path_string(from)), - &import_chunks, - ",", - )?; - fmt.write_chunks_separated(&import_chunks, ",", multiline)?; - Ok(()) - }, - )?; - - self.grouped(|fmt| { - write_chunk!(fmt, imports_start, from.loc().start(), "from")?; - fmt.write_quoted_str(from.loc(), None, &import_path_string(from))?; - fmt.write_semicolon()?; - Ok(()) - })?; - - Ok(()) - } - - #[instrument(name = "enum", skip_all)] - fn visit_enum(&mut self, enumeration: &mut EnumDefinition) -> Result<()> { - return_source_if_disabled!(self, enumeration.loc); - - let enum_name = enumeration.name.safe_unwrap_mut(); - let mut name = - self.visit_to_chunk(enum_name.loc.start(), Some(enum_name.loc.end()), enum_name)?; - name.content = format!("enum {} ", name.content); - if enumeration.values.is_empty() { - self.write_chunk(&name)?; - self.write_empty_brackets()?; - } else { - name.content.push('{'); - self.write_chunk(&name)?; - - self.indented(1, |fmt| { - let values = fmt.items_to_chunks( - Some(enumeration.loc.end()), - enumeration.values.iter_mut().map(|ident| { - let ident = ident.safe_unwrap_mut(); - (ident.loc, ident) - }), - )?; - fmt.write_chunks_separated(&values, ",", true)?; - writeln!(fmt.buf())?; - Ok(()) - })?; - write_chunk!(self, "}}")?; - } - - Ok(()) - } - - #[instrument(name = "assembly", skip_all)] - fn visit_assembly( - &mut self, - loc: Loc, - dialect: &mut Option, - block: &mut YulBlock, - flags: &mut Option>, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - - write_chunk!(self, loc.start(), "assembly")?; - if let Some(StringLiteral { loc, string, .. }) = dialect { - write_chunk!(self, loc.start(), loc.end(), "\"{string}\"")?; - } - if let Some(flags) = flags - && !flags.is_empty() - { - let loc_start = flags.first().unwrap().loc.start(); - self.surrounded( - SurroundingChunk::new("(", Some(loc_start), None), - SurroundingChunk::new(")", None, Some(block.loc.start())), - |fmt, _| { - let mut flags = flags.iter_mut().peekable(); - let mut chunks = vec![]; - while let Some(flag) = flags.next() { - let next_byte_offset = flags.peek().map(|next_flag| next_flag.loc.start()); - chunks.push(fmt.chunked(flag.loc.start(), next_byte_offset, |fmt| { - write!(fmt.buf(), "\"{}\"", flag.string)?; - Ok(()) - })?); - } - fmt.write_chunks_separated(&chunks, ",", false)?; - Ok(()) - }, - )?; - } - - block.visit(self) - } - - #[instrument(name = "block", skip_all)] - fn visit_block( - &mut self, - loc: Loc, - unchecked: bool, - statements: &mut Vec, - ) -> Result<()> { - return_source_if_disabled!(self, loc); - if unchecked { - write_chunk!(self, loc.start(), "unchecked ")?; - } - - self.visit_block(loc, statements, false, false)?; - Ok(()) - } - - #[instrument(name = "args", skip_all)] - fn visit_args(&mut self, loc: Loc, args: &mut Vec) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - - write!(self.buf(), "{{")?; - - let mut args_iter = args.iter_mut().peekable(); - let mut chunks = Vec::new(); - while let Some(NamedArgument { loc: arg_loc, name, expr }) = args_iter.next() { - let next_byte_offset = args_iter - .peek() - .map(|NamedArgument { loc: arg_loc, .. }| arg_loc.start()) - .unwrap_or_else(|| loc.end()); - chunks.push(self.chunked(arg_loc.start(), Some(next_byte_offset), |fmt| { - fmt.grouped(|fmt| { - write_chunk!(fmt, name.loc.start(), "{}: ", name.name)?; - expr.visit(fmt) - })?; - Ok(()) - })?); - } - - if let Some(first) = chunks.first_mut() - && first.prefixes.is_empty() - && first.postfixes_before.is_empty() - && !self.config.bracket_spacing - { - first.needs_space = Some(false); - } - let multiline = self.are_chunks_separated_multiline("{}}", &chunks, ",")?; - self.indented_if(multiline, 1, |fmt| fmt.write_chunks_separated(&chunks, ",", multiline))?; - - let prefix = if multiline && !self.is_beginning_of_line() { - "\n" - } else if self.config.bracket_spacing { - " " - } else { - "" - }; - let closing_bracket = format!("{prefix}{}", "}"); - if let Some(arg) = args.last() { - write_chunk!(self, arg.loc.end(), "{closing_bracket}")?; - } else { - write_chunk!(self, "{closing_bracket}")?; - } - - Ok(()) - } - - #[instrument(name = "expr", skip_all)] - fn visit_expr(&mut self, loc: Loc, expr: &mut Expression) -> Result<()> { - return_source_if_disabled!(self, loc); - - match expr { - Expression::Type(loc, ty) => match ty { - Type::Address => write_chunk!(self, loc.start(), "address")?, - Type::AddressPayable => write_chunk!(self, loc.start(), "address payable")?, - Type::Payable => write_chunk!(self, loc.start(), "payable")?, - Type::Bool => write_chunk!(self, loc.start(), "bool")?, - Type::String => write_chunk!(self, loc.start(), "string")?, - Type::Bytes(n) => write_chunk!(self, loc.start(), "bytes{}", n)?, - Type::Rational => write_chunk!(self, loc.start(), "rational")?, - Type::DynamicBytes => write_chunk!(self, loc.start(), "bytes")?, - &mut Type::Int(ref n) | &mut Type::Uint(ref n) => { - let int = if matches!(ty, Type::Int(_)) { "int" } else { "uint" }; - match n { - 256 => match self.config.int_types { - IntTypes::Long => write_chunk!(self, loc.start(), "{int}{n}")?, - IntTypes::Short => write_chunk!(self, loc.start(), "{int}")?, - IntTypes::Preserve => self.visit_source(*loc)?, - }, - _ => write_chunk!(self, loc.start(), "{int}{n}")?, - } - } - Type::Mapping { loc, key, key_name, value, value_name } => { - let arrow_loc = self.find_next_str_in_src(loc.start(), "=>"); - let close_paren_loc = - self.find_next_in_src(value.loc().end(), ')').unwrap_or(loc.end()); - let first = SurroundingChunk::new( - "mapping(", - Some(loc.start()), - Some(key.loc().start()), - ); - let last = SurroundingChunk::new(")", Some(close_paren_loc), Some(loc.end())) - .non_spaced(); - self.surrounded(first, last, |fmt, multiline| { - fmt.grouped(|fmt| { - key.visit(fmt)?; - - if let Some(name) = key_name { - let end_loc = arrow_loc.unwrap_or(value.loc().start()); - write_chunk!(fmt, name.loc.start(), end_loc, " {}", name)?; - } else if let Some(arrow_loc) = arrow_loc { - fmt.write_postfix_comments_before(arrow_loc)?; - } - - let mut write_arrow_and_value = |fmt: &mut Self| { - write!(fmt.buf(), "=> ")?; - value.visit(fmt)?; - if let Some(name) = value_name { - write_chunk!(fmt, name.loc.start(), " {}", name)?; - } - Ok(()) - }; - - let rest_str = fmt.simulate_to_string(&mut write_arrow_and_value)?; - let multiline = multiline && !fmt.will_it_fit(rest_str); - fmt.write_whitespace_separator(multiline)?; - - write_arrow_and_value(fmt)?; - - fmt.write_postfix_comments_before(close_paren_loc)?; - fmt.write_prefix_comments_before(close_paren_loc) - })?; - Ok(()) - })?; - } - Type::Function { .. } => self.visit_source(*loc)?, - }, - Expression::BoolLiteral(loc, val) => { - write_chunk!(self, loc.start(), loc.end(), "{val}")?; - } - Expression::NumberLiteral(loc, val, exp, unit) => { - self.write_num_literal(*loc, val, None, exp, unit)?; - } - Expression::HexNumberLiteral(loc, val, unit) => { - // ref: https://docs.soliditylang.org/en/latest/types.html?highlight=address%20literal#address-literals - let val = if val.len() == 42 { - Address::from_str(val).expect("").to_string() - } else { - val.to_owned() - }; - write_chunk!(self, loc.start(), loc.end(), "{val}")?; - self.write_unit(unit)?; - } - Expression::RationalNumberLiteral(loc, val, fraction, exp, unit) => { - self.write_num_literal(*loc, val, Some(fraction), exp, unit)?; - } - Expression::StringLiteral(vals) => { - for StringLiteral { loc, string, unicode } in vals { - let prefix = if *unicode { Some("unicode") } else { None }; - self.write_quoted_str(*loc, prefix, string)?; - } - } - Expression::HexLiteral(vals) => { - for val in vals { - self.write_hex_literal(val)?; - } - } - Expression::AddressLiteral(loc, val) => { - // support of solana/substrate address literals - self.write_quoted_str(*loc, Some("address"), val)?; - } - Expression::Parenthesis(loc, expr) => { - self.surrounded( - SurroundingChunk::new("(", Some(loc.start()), None), - SurroundingChunk::new(")", None, Some(loc.end())), - |fmt, _| expr.visit(fmt), - )?; - } - Expression::ArraySubscript(_, ty_exp, index_expr) => { - ty_exp.visit(self)?; - write!(self.buf(), "[")?; - index_expr.as_mut().map(|index| index.visit(self)).transpose()?; - write!(self.buf(), "]")?; - } - Expression::ArraySlice(loc, expr, start, end) => { - expr.visit(self)?; - write!(self.buf(), "[")?; - let mut write_slice = |fmt: &mut Self, multiline| -> Result<()> { - if multiline { - fmt.write_whitespace_separator(true)?; - } - fmt.grouped(|fmt| { - start.as_mut().map(|start| start.visit(fmt)).transpose()?; - write!(fmt.buf(), ":")?; - if let Some(end) = end { - let mut chunk = - fmt.chunked(end.loc().start(), Some(loc.end()), |fmt| { - end.visit(fmt) - })?; - if chunk.prefixes.is_empty() - && chunk.postfixes_before.is_empty() - && (start.is_none() || fmt.will_it_fit(&chunk.content)) - { - chunk.needs_space = Some(false); - } - fmt.write_chunk(&chunk)?; - } - Ok(()) - })?; - if multiline { - fmt.write_whitespace_separator(true)?; - } - Ok(()) - }; - - if !self.try_on_single_line(|fmt| write_slice(fmt, false))? { - self.indented(1, |fmt| write_slice(fmt, true))?; - } - - write!(self.buf(), "]")?; - } - Expression::ArrayLiteral(loc, exprs) => { - write_chunk!(self, loc.start(), "[")?; - let chunks = self.items_to_chunks( - Some(loc.end()), - exprs.iter_mut().map(|expr| (expr.loc(), expr)), - )?; - let multiline = self.are_chunks_separated_multiline("{}]", &chunks, ",")?; - self.indented_if(multiline, 1, |fmt| { - fmt.write_chunks_separated(&chunks, ",", multiline)?; - if multiline { - fmt.write_postfix_comments_before(loc.end())?; - fmt.write_prefix_comments_before(loc.end())?; - fmt.write_whitespace_separator(true)?; - } - Ok(()) - })?; - write_chunk!(self, loc.end(), "]")?; - } - Expression::PreIncrement(..) - | Expression::PostIncrement(..) - | Expression::PreDecrement(..) - | Expression::PostDecrement(..) - | Expression::Not(..) - | Expression::UnaryPlus(..) - | Expression::Add(..) - | Expression::Negate(..) - | Expression::Subtract(..) - | Expression::Power(..) - | Expression::Multiply(..) - | Expression::Divide(..) - | Expression::Modulo(..) - | Expression::ShiftLeft(..) - | Expression::ShiftRight(..) - | Expression::BitwiseNot(..) - | Expression::BitwiseAnd(..) - | Expression::BitwiseXor(..) - | Expression::BitwiseOr(..) - | Expression::Less(..) - | Expression::More(..) - | Expression::LessEqual(..) - | Expression::MoreEqual(..) - | Expression::And(..) - | Expression::Or(..) - | Expression::Equal(..) - | Expression::NotEqual(..) => { - let spaced = expr.has_space_around(); - let op = expr.operator().unwrap(); - - match expr.components_mut() { - (Some(left), Some(right)) => { - left.visit(self)?; - - let right_chunk = - self.chunked(right.loc().start(), Some(loc.end()), |fmt| { - write_chunk!(fmt, right.loc().start(), "{op}")?; - right.visit(fmt)?; - Ok(()) - })?; - - self.grouped(|fmt| fmt.write_chunk(&right_chunk))?; - } - (Some(left), None) => { - left.visit(self)?; - write_chunk_spaced!(self, loc.end(), Some(spaced), "{op}")?; - } - (None, Some(right)) => { - write_chunk!(self, right.loc().start(), "{op}")?; - let mut right_chunk = - self.visit_to_chunk(right.loc().end(), Some(loc.end()), right)?; - right_chunk.needs_space = Some(spaced); - self.write_chunk(&right_chunk)?; - } - (None, None) => {} - } - } - Expression::Assign(..) - | Expression::AssignOr(..) - | Expression::AssignAnd(..) - | Expression::AssignXor(..) - | Expression::AssignShiftLeft(..) - | Expression::AssignShiftRight(..) - | Expression::AssignAdd(..) - | Expression::AssignSubtract(..) - | Expression::AssignMultiply(..) - | Expression::AssignDivide(..) - | Expression::AssignModulo(..) => { - let op = expr.operator().unwrap(); - let (left, right) = expr.components_mut(); - let (left, right) = (left.unwrap(), right.unwrap()); - - left.visit(self)?; - write_chunk!(self, "{op}")?; - self.visit_assignment(right)?; - } - Expression::ConditionalOperator(loc, cond, first_expr, second_expr) => { - cond.visit(self)?; - - let first_expr = self.chunked( - first_expr.loc().start(), - Some(second_expr.loc().start()), - |fmt| { - write_chunk!(fmt, "?")?; - first_expr.visit(fmt) - }, - )?; - let second_expr = - self.chunked(second_expr.loc().start(), Some(loc.end()), |fmt| { - write_chunk!(fmt, ":")?; - second_expr.visit(fmt) - })?; - - let chunks = vec![first_expr, second_expr]; - if !self.try_on_single_line(|fmt| fmt.write_chunks_separated(&chunks, "", false))? { - self.grouped(|fmt| fmt.write_chunks_separated(&chunks, "", true))?; - } - } - Expression::Variable(ident) => { - write_chunk!(self, loc.end(), "{}", ident.name)?; - } - Expression::MemberAccess(_, expr, ident) => { - self.visit_member_access(expr, ident, |fmt, expr| match expr.as_mut() { - Expression::MemberAccess(_, inner_expr, inner_ident) => { - Ok(Some((inner_expr, inner_ident))) - } - expr => { - expr.visit(fmt)?; - Ok(None) - } - })?; - } - Expression::List(loc, items) => { - self.surrounded( - SurroundingChunk::new( - "(", - Some(loc.start()), - items.first().map(|item| item.0.start()), - ), - SurroundingChunk::new(")", None, Some(loc.end())), - |fmt, _| { - let items = fmt.items_to_chunks( - Some(loc.end()), - items.iter_mut().map(|(loc, item)| (*loc, item)), - )?; - let write_items = |fmt: &mut Self, multiline| { - fmt.write_chunks_separated(&items, ",", multiline) - }; - if !fmt.try_on_single_line(|fmt| write_items(fmt, false))? { - write_items(fmt, true)?; - } - Ok(()) - }, - )?; - } - Expression::FunctionCall(loc, expr, exprs) => { - self.visit_expr(expr.loc(), expr)?; - self.visit_list("", exprs, Some(expr.loc().end()), Some(loc.end()), true)?; - } - Expression::NamedFunctionCall(loc, expr, args) => { - self.visit_expr(expr.loc(), expr)?; - write!(self.buf(), "(")?; - self.visit_args(*loc, args)?; - write!(self.buf(), ")")?; - } - Expression::FunctionCallBlock(_, expr, stmt) => { - expr.visit(self)?; - stmt.visit(self)?; - } - Expression::New(_, expr) => { - write_chunk!(self, "new ")?; - self.visit_expr(expr.loc(), expr)?; - } - _ => self.visit_source(loc)?, - }; - - Ok(()) - } - - #[instrument(name = "ident", skip_all)] - fn visit_ident(&mut self, loc: Loc, ident: &mut Identifier) -> Result<()> { - return_source_if_disabled!(self, loc); - write_chunk!(self, loc.end(), "{}", ident.name)?; - Ok(()) - } - - #[instrument(name = "ident_path", skip_all)] - fn visit_ident_path(&mut self, idents: &mut IdentifierPath) -> Result<(), Self::Error> { - if idents.identifiers.is_empty() { - return Ok(()); - } - return_source_if_disabled!(self, idents.loc); - - idents.identifiers.iter_mut().skip(1).for_each(|chunk| { - if !chunk.name.starts_with('.') { - chunk.name.insert(0, '.') - } - }); - let chunks = self.items_to_chunks( - Some(idents.loc.end()), - idents.identifiers.iter_mut().map(|ident| (ident.loc, ident)), - )?; - self.grouped(|fmt| { - let multiline = fmt.are_chunks_separated_multiline("{}", &chunks, "")?; - fmt.write_chunks_separated(&chunks, "", multiline) - })?; - Ok(()) - } - - #[instrument(name = "emit", skip_all)] - fn visit_emit(&mut self, loc: Loc, event: &mut Expression) -> Result<()> { - return_source_if_disabled!(self, loc); - write_chunk!(self, loc.start(), "emit")?; - event.visit(self)?; - self.write_semicolon()?; - Ok(()) - } - - #[instrument(name = "var_definition", skip_all)] - fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<()> { - return_source_if_disabled!(self, var.loc, ';'); - - var.ty.visit(self)?; - - let multiline = self.grouped(|fmt| { - let var_name = var.name.safe_unwrap_mut(); - let name_start = var_name.loc.start(); - - let attrs = fmt.items_to_chunks_sorted(Some(name_start), var.attrs.iter_mut())?; - if !fmt.try_on_single_line(|fmt| fmt.write_chunks_separated(&attrs, "", false))? { - fmt.write_chunks_separated(&attrs, "", true)?; - } - - let mut name = fmt.visit_to_chunk(name_start, Some(var_name.loc.end()), var_name)?; - if var.initializer.is_some() { - name.content.push_str(" ="); - } - fmt.write_chunk(&name)?; - - Ok(()) - })?; - - var.initializer - .as_mut() - .map(|init| self.indented_if(multiline, 1, |fmt| fmt.visit_assignment(init))) - .transpose()?; - - self.write_semicolon()?; - - Ok(()) - } - - #[instrument(name = "var_definition_stmt", skip_all)] - fn visit_var_definition_stmt( - &mut self, - loc: Loc, - declaration: &mut VariableDeclaration, - expr: &mut Option, - ) -> Result<()> { - return_source_if_disabled!(self, loc, ';'); - - let declaration = self - .chunked(declaration.loc.start(), None, |fmt| fmt.visit_var_declaration(declaration))?; - let multiline = declaration.content.contains('\n'); - self.write_chunk(&declaration)?; - - if let Some(expr) = expr { - write!(self.buf(), " =")?; - self.indented_if(multiline, 1, |fmt| fmt.visit_assignment(expr))?; - } - - self.write_semicolon() - } - - #[instrument(name = "var_declaration", skip_all)] - fn visit_var_declaration(&mut self, var: &mut VariableDeclaration) -> Result<()> { - return_source_if_disabled!(self, var.loc); - self.grouped(|fmt| { - var.ty.visit(fmt)?; - if let Some(storage) = &var.storage { - write_chunk!(fmt, storage.loc().end(), "{storage}")?; - } - let var_name = var.name.safe_unwrap(); - write_chunk!(fmt, var_name.loc.end(), "{var_name}") - })?; - Ok(()) - } - - #[instrument(name = "return", skip_all)] - fn visit_return(&mut self, loc: Loc, expr: &mut Option) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc, ';'); - - self.write_postfix_comments_before(loc.start())?; - self.write_prefix_comments_before(loc.start())?; - - if expr.is_none() { - write_chunk!(self, loc.end(), "return;")?; - return Ok(()); - } - - let expr = expr.as_mut().unwrap(); - let expr_loc_start = expr.loc().start(); - let write_return = |fmt: &mut Self| -> Result<()> { - write_chunk!(fmt, loc.start(), "return")?; - fmt.write_postfix_comments_before(expr_loc_start)?; - Ok(()) - }; - - let mut write_return_with_expr = |fmt: &mut Self| -> Result<()> { - let fits_on_single = fmt.try_on_single_line(|fmt| { - write_return(fmt)?; - expr.visit(fmt) - })?; - if fits_on_single { - return Ok(()); - } - - let mut fit_on_next_line = false; - let tx = fmt.transact(|fmt| { - fmt.grouped(|fmt| { - write_return(fmt)?; - if !fmt.is_beginning_of_line() { - fmt.write_whitespace_separator(true)?; - } - fit_on_next_line = fmt.try_on_single_line(|fmt| expr.visit(fmt))?; - Ok(()) - })?; - Ok(()) - })?; - if fit_on_next_line { - tx.commit()?; - return Ok(()); - } - - write_return(fmt)?; - expr.visit(fmt)?; - Ok(()) - }; - - write_return_with_expr(self)?; - write_chunk!(self, loc.end(), ";")?; - Ok(()) - } - - #[instrument(name = "revert", skip_all)] - fn visit_revert( - &mut self, - loc: Loc, - error: &mut Option, - args: &mut Vec, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc, ';'); - write_chunk!(self, loc.start(), "revert")?; - if let Some(error) = error { - error.visit(self)?; - } - self.visit_list("", args, None, Some(loc.end()), true)?; - self.write_semicolon()?; - - Ok(()) - } - - #[instrument(name = "revert_named_args", skip_all)] - fn visit_revert_named_args( - &mut self, - loc: Loc, - error: &mut Option, - args: &mut Vec, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc, ';'); - - write_chunk!(self, loc.start(), "revert")?; - let mut error_indented = false; - if let Some(error) = error - && !self.try_on_single_line(|fmt| error.visit(fmt))? - { - error.visit(self)?; - error_indented = true; - } - - if args.is_empty() { - write!(self.buf(), "({{}});")?; - return Ok(()); - } - - write!(self.buf(), "(")?; - self.indented_if(error_indented, 1, |fmt| fmt.visit_args(loc, args))?; - write!(self.buf(), ")")?; - self.write_semicolon()?; - - Ok(()) - } - - #[instrument(name = "break", skip_all)] - fn visit_break(&mut self, loc: Loc, semicolon: bool) -> Result<()> { - if semicolon { - return_source_if_disabled!(self, loc, ';'); - } else { - return_source_if_disabled!(self, loc); - } - write_chunk!(self, loc.start(), loc.end(), "break{}", if semicolon { ";" } else { "" }) - } - - #[instrument(name = "continue", skip_all)] - fn visit_continue(&mut self, loc: Loc, semicolon: bool) -> Result<()> { - if semicolon { - return_source_if_disabled!(self, loc, ';'); - } else { - return_source_if_disabled!(self, loc); - } - write_chunk!(self, loc.start(), loc.end(), "continue{}", if semicolon { ";" } else { "" }) - } - - #[instrument(name = "try", skip_all)] - fn visit_try( - &mut self, - loc: Loc, - expr: &mut Expression, - returns: &mut Option<(Vec<(Loc, Option)>, Box)>, - clauses: &mut Vec, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - - let try_next_byte = clauses.first().map(|c| match c { - CatchClause::Simple(loc, ..) => loc.start(), - CatchClause::Named(loc, ..) => loc.start(), - }); - let try_chunk = self.chunked(loc.start(), try_next_byte, |fmt| { - write_chunk!(fmt, loc.start(), expr.loc().start(), "try")?; - expr.visit(fmt)?; - if let Some((params, stmt)) = returns { - let mut params = - params.iter_mut().filter(|(_, param)| param.is_some()).collect::>(); - let byte_offset = params.first().map_or(stmt.loc().start(), |p| p.0.start()); - fmt.surrounded( - SurroundingChunk::new("returns (", Some(byte_offset), None), - SurroundingChunk::new(")", None, params.last().map(|p| p.0.end())), - |fmt, _| { - let chunks = fmt.items_to_chunks( - Some(stmt.loc().start()), - params.iter_mut().map(|(loc, ident)| (*loc, ident)), - )?; - let multiline = fmt.are_chunks_separated_multiline("{})", &chunks, ",")?; - fmt.write_chunks_separated(&chunks, ",", multiline)?; - Ok(()) - }, - )?; - stmt.visit(fmt)?; - } - Ok(()) - })?; - - let mut chunks = vec![try_chunk]; - for clause in clauses { - let (loc, ident, mut param, stmt) = match clause { - CatchClause::Simple(loc, param, stmt) => (loc, None, param.as_mut(), stmt), - CatchClause::Named(loc, ident, param, stmt) => { - (loc, Some(ident), Some(param), stmt) - } - }; - - let chunk = self.chunked(loc.start(), Some(stmt.loc().start()), |fmt| { - write_chunk!(fmt, "catch")?; - if let Some(ident) = ident.as_ref() { - fmt.write_postfix_comments_before( - param.as_ref().map(|p| p.loc.start()).unwrap_or_else(|| ident.loc.end()), - )?; - write_chunk!(fmt, ident.loc.start(), "{}", ident.name)?; - } - if let Some(param) = param.as_mut() { - write_chunk_spaced!(fmt, param.loc.start(), Some(ident.is_none()), "(")?; - fmt.surrounded( - SurroundingChunk::new("", Some(param.loc.start()), None), - SurroundingChunk::new(")", None, Some(stmt.loc().start())), - |fmt, _| param.visit(fmt), - )?; - } - - stmt.visit(fmt)?; - Ok(()) - })?; - - chunks.push(chunk); - } - - let multiline = self.are_chunks_separated_multiline("{}", &chunks, "")?; - if !multiline { - self.write_chunks_separated(&chunks, "", false)?; - return Ok(()); - } - - let mut chunks = chunks.iter_mut().peekable(); - let mut prev_multiline = false; - - // write try chunk first - if let Some(chunk) = chunks.next() { - let chunk_str = self.simulate_to_string(|fmt| fmt.write_chunk(chunk))?; - write!(self.buf(), "{chunk_str}")?; - prev_multiline = chunk_str.contains('\n'); - } - - while let Some(chunk) = chunks.next() { - let chunk_str = self.simulate_to_string(|fmt| fmt.write_chunk(chunk))?; - let multiline = chunk_str.contains('\n'); - self.indented_if(!multiline, 1, |fmt| { - chunk.needs_space = Some(false); - let on_same_line = prev_multiline && (multiline || chunks.peek().is_none()); - let prefix = if fmt.is_beginning_of_line() { - "" - } else if on_same_line { - " " - } else { - "\n" - }; - let chunk_str = format!("{prefix}{chunk_str}"); - write!(fmt.buf(), "{chunk_str}")?; - Ok(()) - })?; - prev_multiline = multiline; - } - Ok(()) - } - - #[instrument(name = "if", skip_all)] - fn visit_if( - &mut self, - loc: Loc, - cond: &mut Expression, - if_branch: &mut Box, - else_branch: &mut Option>, - is_first_stmt: bool, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - - if !is_first_stmt { - self.write_if_stmt(loc, cond, if_branch, else_branch)?; - return Ok(()); - } - - self.context.if_stmt_single_line = Some(true); - let mut stmt_fits_on_single = false; - let tx = self.transact(|fmt| { - stmt_fits_on_single = match fmt.write_if_stmt(loc, cond, if_branch, else_branch) { - Ok(()) => true, - Err(FormatterError::Fmt(_)) => false, - Err(err) => bail!(err), - }; - Ok(()) - })?; - - if stmt_fits_on_single { - tx.commit()?; - } else { - self.context.if_stmt_single_line = Some(false); - self.write_if_stmt(loc, cond, if_branch, else_branch)?; - } - self.context.if_stmt_single_line = None; - - Ok(()) - } - - #[instrument(name = "do_while", skip_all)] - fn visit_do_while( - &mut self, - loc: Loc, - body: &mut Statement, - cond: &mut Expression, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc, ';'); - write_chunk!(self, loc.start(), "do ")?; - self.visit_stmt_as_block(body, false)?; - visit_source_if_disabled_else!(self, loc.with_start(body.loc().end()), { - self.surrounded( - SurroundingChunk::new("while (", Some(cond.loc().start()), None), - SurroundingChunk::new(");", None, Some(loc.end())), - |fmt, _| cond.visit(fmt), - )?; - }); - Ok(()) - } - - #[instrument(name = "while", skip_all)] - fn visit_while( - &mut self, - loc: Loc, - cond: &mut Expression, - body: &mut Statement, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - self.surrounded( - SurroundingChunk::new("while (", Some(loc.start()), None), - SurroundingChunk::new(")", None, Some(cond.loc().end())), - |fmt, _| { - cond.visit(fmt)?; - fmt.write_postfix_comments_before(body.loc().start()) - }, - )?; - - let cond_close_paren_loc = - self.find_next_in_src(cond.loc().end(), ')').unwrap_or_else(|| cond.loc().end()); - let attempt_single_line = self.should_attempt_block_single_line(body, cond_close_paren_loc); - self.visit_stmt_as_block(body, attempt_single_line)?; - Ok(()) - } - - #[instrument(name = "for", skip_all)] - fn visit_for( - &mut self, - loc: Loc, - init: &mut Option>, - cond: &mut Option>, - update: &mut Option>, - body: &mut Option>, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - - let next_byte_end = update.as_ref().map(|u| u.loc().end()); - self.surrounded( - SurroundingChunk::new("for (", Some(loc.start()), None), - SurroundingChunk::new(")", None, next_byte_end), - |fmt, _| { - let mut write_for_loop_header = |fmt: &mut Self, multiline: bool| -> Result<()> { - match init { - Some(stmt) => stmt.visit(fmt), - None => fmt.write_semicolon(), - }?; - if multiline { - fmt.write_whitespace_separator(true)?; - } - - cond.visit(fmt)?; - fmt.write_semicolon()?; - if multiline { - fmt.write_whitespace_separator(true)?; - } - - match update { - Some(expr) => expr.visit(fmt), - None => Ok(()), - } - }; - let multiline = !fmt.try_on_single_line(|fmt| write_for_loop_header(fmt, false))?; - if multiline { - write_for_loop_header(fmt, true)?; - } - Ok(()) - }, - )?; - match body { - Some(body) => { - self.visit_stmt_as_block(body, false)?; - } - None => { - self.write_empty_brackets()?; - } - }; - Ok(()) - } - - #[instrument(name = "function", skip_all)] - fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { - if func.body.is_some() { - return_source_if_disabled!(self, func.loc()); - } else { - return_source_if_disabled!(self, func.loc(), ';'); - } - - self.with_function_context(func.clone(), |fmt| { - fmt.write_postfix_comments_before(func.loc.start())?; - fmt.write_prefix_comments_before(func.loc.start())?; - - let body_loc = func.body.as_ref().map(CodeLocation::loc); - let mut attrs_multiline = false; - let fits_on_single = fmt.try_on_single_line(|fmt| { - fmt.write_function_header(func, body_loc, false)?; - Ok(()) - })?; - if !fits_on_single { - attrs_multiline = fmt.write_function_header(func, body_loc, true)?; - } - - // write function body - match &mut func.body { - Some(body) => { - let body_loc = body.loc(); - // Handle case where block / statements starts on disabled line. - if fmt.inline_config.is_disabled(body_loc.with_end(body_loc.start())) { - match body { - Statement::Block { statements, .. } if !statements.is_empty() => { - fmt.write_whitespace_separator(false)?; - fmt.visit_block(body_loc, statements, false, false)?; - return Ok(()); - } - _ => { - // Attrs should be written on same line if first line is disabled - // and there's no statement. - attrs_multiline = false - } - } - } - - let byte_offset = body_loc.start(); - let body = fmt.visit_to_chunk(byte_offset, Some(body_loc.end()), body)?; - fmt.write_whitespace_separator( - attrs_multiline && !(func.attributes.is_empty() && func.returns.is_empty()), - )?; - fmt.write_chunk(&body)?; - } - None => fmt.write_semicolon()?, - } - Ok(()) - })?; - - Ok(()) - } - - #[instrument(name = "function_attribute", skip_all)] - fn visit_function_attribute(&mut self, attribute: &mut FunctionAttribute) -> Result<()> { - return_source_if_disabled!(self, attribute.loc()); - - match attribute { - FunctionAttribute::Mutability(mutability) => { - write_chunk!(self, mutability.loc().end(), "{mutability}")? - } - FunctionAttribute::Visibility(visibility) => { - // Visibility will always have a location in a Function attribute - write_chunk!(self, visibility.loc_opt().unwrap().end(), "{visibility}")? - } - FunctionAttribute::Virtual(loc) => write_chunk!(self, loc.end(), "virtual")?, - FunctionAttribute::Immutable(loc) => write_chunk!(self, loc.end(), "immutable")?, - FunctionAttribute::Override(loc, args) => { - write_chunk!(self, loc.start(), "override")?; - if !args.is_empty() && self.config.override_spacing { - self.write_whitespace_separator(false)?; - } - self.visit_list("", args, None, Some(loc.end()), false)? - } - FunctionAttribute::BaseOrModifier(loc, base) => { - // here we need to find out if this attribute belongs to the constructor because the - // modifier need to include the trailing parenthesis - // This is very ambiguous because the modifier can either by an inherited contract - // or a modifier here: e.g.: This is valid constructor: - // `constructor() public Ownable() OnlyOwner {}` - let is_constructor = self.context.is_constructor_function(); - // we can't make any decisions here regarding trailing `()` because we'd need to - // find out if the `base` is a solidity modifier or an - // interface/contract therefore we use its raw content. - - // we can however check if the contract `is` the `base`, this however also does - // not cover all cases - let is_contract_base = self.context.contract.as_ref().is_some_and(|contract| { - contract.base.iter().any(|contract_base| { - contract_base - .name - .identifiers - .iter() - .zip(&base.name.identifiers) - .all(|(l, r)| l.name == r.name) - }) - }); - - if is_contract_base { - base.visit(self)?; - } else if is_constructor { - // This is ambiguous because the modifier can either by an inherited - // contract modifiers with empty parenthesis are - // valid, but not required so we make the assumption - // here that modifiers are lowercase - let mut base_or_modifier = - self.visit_to_chunk(loc.start(), Some(loc.end()), base)?; - let is_lowercase = - base_or_modifier.content.chars().next().is_some_and(|c| c.is_lowercase()); - if is_lowercase && base_or_modifier.content.ends_with("()") { - base_or_modifier.content.truncate(base_or_modifier.content.len() - 2); - } - - self.write_chunk(&base_or_modifier)?; - } else { - let mut base_or_modifier = - self.visit_to_chunk(loc.start(), Some(loc.end()), base)?; - if base_or_modifier.content.ends_with("()") { - base_or_modifier.content.truncate(base_or_modifier.content.len() - 2); - } - self.write_chunk(&base_or_modifier)?; - } - } - FunctionAttribute::Error(loc) => self.visit_parser_error(*loc)?, - }; - - Ok(()) - } - - #[instrument(name = "var_attribute", skip_all)] - fn visit_var_attribute(&mut self, attribute: &mut VariableAttribute) -> Result<()> { - return_source_if_disabled!(self, attribute.loc()); - - let token = match attribute { - VariableAttribute::Visibility(visibility) => Some(visibility.to_string()), - VariableAttribute::Constant(_) => Some("constant".to_string()), - VariableAttribute::Immutable(_) => Some("immutable".to_string()), - VariableAttribute::StorageType(_) => None, // Unsupported - VariableAttribute::Override(loc, idents) => { - write_chunk!(self, loc.start(), "override")?; - if !idents.is_empty() && self.config.override_spacing { - self.write_whitespace_separator(false)?; - } - self.visit_list("", idents, Some(loc.start()), Some(loc.end()), false)?; - None - } - VariableAttribute::StorageLocation(storage) => Some(storage.to_string()), - }; - if let Some(token) = token { - let loc = attribute.loc(); - write_chunk!(self, loc.start(), loc.end(), "{}", token)?; - } - Ok(()) - } - - #[instrument(name = "base", skip_all)] - fn visit_base(&mut self, base: &mut Base) -> Result<()> { - return_source_if_disabled!(self, base.loc); - - let name_loc = &base.name.loc; - let mut name = self.chunked(name_loc.start(), Some(name_loc.end()), |fmt| { - fmt.visit_ident_path(&mut base.name)?; - Ok(()) - })?; - - if base.args.is_none() || base.args.as_ref().unwrap().is_empty() { - // This is ambiguous because the modifier can either by an inherited contract or a - // modifier - if self.context.function.is_some() { - name.content.push_str("()"); - } - self.write_chunk(&name)?; - return Ok(()); - } - - let args = base.args.as_mut().unwrap(); - let args_start = CodeLocation::loc(args.first().unwrap()).start(); - - name.content.push('('); - let formatted_name = self.chunk_to_string(&name)?; - - let multiline = !self.will_it_fit(&formatted_name); - - self.surrounded( - SurroundingChunk::new(&formatted_name, Some(args_start), None), - SurroundingChunk::new(")", None, Some(base.loc.end())), - |fmt, multiline_hint| { - let args = fmt.items_to_chunks( - Some(base.loc.end()), - args.iter_mut().map(|arg| (arg.loc(), arg)), - )?; - let multiline = multiline - || multiline_hint - || fmt.are_chunks_separated_multiline("{}", &args, ",")?; - fmt.write_chunks_separated(&args, ",", multiline)?; - Ok(()) - }, - )?; - - Ok(()) - } - - #[instrument(name = "parameter", skip_all)] - fn visit_parameter(&mut self, parameter: &mut Parameter) -> Result<()> { - return_source_if_disabled!(self, parameter.loc); - self.grouped(|fmt| { - parameter.ty.visit(fmt)?; - if let Some(storage) = ¶meter.storage { - write_chunk!(fmt, storage.loc().end(), "{storage}")?; - } - if let Some(name) = ¶meter.name { - write_chunk!(fmt, parameter.loc.end(), "{}", name.name)?; - } - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "struct", skip_all)] - fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { - return_source_if_disabled!(self, structure.loc); - self.grouped(|fmt| { - let struct_name = structure.name.safe_unwrap_mut(); - write_chunk!(fmt, struct_name.loc.start(), "struct")?; - struct_name.visit(fmt)?; - if structure.fields.is_empty() { - return fmt.write_empty_brackets(); - } - - write!(fmt.buf(), " {{")?; - fmt.surrounded( - SurroundingChunk::new("", Some(struct_name.loc.end()), None), - SurroundingChunk::new("}", None, Some(structure.loc.end())), - |fmt, _multiline| { - let chunks = fmt.items_to_chunks( - Some(structure.loc.end()), - structure.fields.iter_mut().map(|ident| (ident.loc, ident)), - )?; - for mut chunk in chunks { - chunk.content.push(';'); - fmt.write_chunk(&chunk)?; - fmt.write_whitespace_separator(true)?; - } - Ok(()) - }, - ) - })?; - - Ok(()) - } - - #[instrument(name = "event", skip_all)] - fn visit_event(&mut self, event: &mut EventDefinition) -> Result<()> { - return_source_if_disabled!(self, event.loc, ';'); - - let event_name = event.name.safe_unwrap_mut(); - let mut name = - self.visit_to_chunk(event_name.loc.start(), Some(event.loc.end()), event_name)?; - name.content = format!("event {}(", name.content); - - let last_chunk = if event.anonymous { ") anonymous;" } else { ");" }; - if event.fields.is_empty() { - name.content.push_str(last_chunk); - self.write_chunk(&name)?; - } else { - let byte_offset = event.fields.first().unwrap().loc.start(); - let first_chunk = self.chunk_to_string(&name)?; - self.surrounded( - SurroundingChunk::new(first_chunk, Some(byte_offset), None), - SurroundingChunk::new(last_chunk, None, Some(event.loc.end())), - |fmt, multiline| { - let params = fmt - .items_to_chunks(None, event.fields.iter_mut().map(|arg| (arg.loc, arg)))?; - - let multiline = - multiline && fmt.are_chunks_separated_multiline("{}", ¶ms, ",")?; - fmt.write_chunks_separated(¶ms, ",", multiline) - }, - )?; - } - - Ok(()) - } - - #[instrument(name = "event_parameter", skip_all)] - fn visit_event_parameter(&mut self, param: &mut EventParameter) -> Result<()> { - return_source_if_disabled!(self, param.loc); - - self.grouped(|fmt| { - param.ty.visit(fmt)?; - if param.indexed { - write_chunk!(fmt, param.loc.start(), "indexed")?; - } - if let Some(name) = ¶m.name { - write_chunk!(fmt, name.loc.end(), "{}", name.name)?; - } - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "error", skip_all)] - fn visit_error(&mut self, error: &mut ErrorDefinition) -> Result<()> { - return_source_if_disabled!(self, error.loc, ';'); - - let error_name = error.name.safe_unwrap_mut(); - let mut name = self.visit_to_chunk(error_name.loc.start(), None, error_name)?; - name.content = format!("error {}", name.content); - - let formatted_name = self.chunk_to_string(&name)?; - write!(self.buf(), "{formatted_name}")?; - let start_offset = error.fields.first().map(|f| f.loc.start()); - self.visit_list("", &mut error.fields, start_offset, Some(error.loc.end()), true)?; - self.write_semicolon()?; - - Ok(()) - } - - #[instrument(name = "error_parameter", skip_all)] - fn visit_error_parameter(&mut self, param: &mut ErrorParameter) -> Result<()> { - return_source_if_disabled!(self, param.loc); - self.grouped(|fmt| { - param.ty.visit(fmt)?; - if let Some(name) = ¶m.name { - write_chunk!(fmt, name.loc.end(), "{}", name.name)?; - } - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "type_definition", skip_all)] - fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> Result<()> { - return_source_if_disabled!(self, def.loc, ';'); - self.grouped(|fmt| { - write_chunk!(fmt, def.loc.start(), def.name.loc.start(), "type")?; - def.name.visit(fmt)?; - write_chunk!(fmt, def.name.loc.end(), CodeLocation::loc(&def.ty).start(), "is")?; - def.ty.visit(fmt)?; - fmt.write_semicolon()?; - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "stray_semicolon", skip_all)] - fn visit_stray_semicolon(&mut self) -> Result<()> { - self.write_semicolon() - } - - #[instrument(name = "opening_paren", skip_all)] - fn visit_opening_paren(&mut self) -> Result<()> { - write_chunk!(self, "(")?; - Ok(()) - } - - #[instrument(name = "closing_paren", skip_all)] - fn visit_closing_paren(&mut self) -> Result<()> { - write_chunk!(self, ")")?; - Ok(()) - } - - #[instrument(name = "newline", skip_all)] - fn visit_newline(&mut self) -> Result<()> { - writeln_chunk!(self)?; - Ok(()) - } - - #[instrument(name = "using", skip_all)] - fn visit_using(&mut self, using: &mut Using) -> Result<()> { - return_source_if_disabled!(self, using.loc, ';'); - - write_chunk!(self, using.loc.start(), "using")?; - - let ty_start = using.ty.as_mut().map(|ty| CodeLocation::loc(&ty).start()); - let global_start = using.global.as_mut().map(|global| global.loc.start()); - let loc_end = using.loc.end(); - - let (is_library, mut list_chunks) = match &mut using.list { - UsingList::Library(library) => { - (true, vec![self.visit_to_chunk(library.loc.start(), None, library)?]) - } - UsingList::Functions(funcs) => { - let mut funcs = funcs.iter_mut().peekable(); - let mut chunks = Vec::new(); - while let Some(func) = funcs.next() { - let next_byte_end = funcs.peek().map(|func| func.loc.start()); - chunks.push(self.chunked(func.loc.start(), next_byte_end, |fmt| { - fmt.visit_ident_path(&mut func.path)?; - if let Some(op) = func.oper { - write!(fmt.buf(), " as {op}")?; - } - Ok(()) - })?); - } - (false, chunks) - } - UsingList::Error => return self.visit_parser_error(using.loc), - }; - - let for_chunk = self.chunk_at( - using.loc.start(), - Some(ty_start.or(global_start).unwrap_or(loc_end)), - None, - "for", - ); - let ty_chunk = if let Some(ty) = &mut using.ty { - self.visit_to_chunk(ty.loc().start(), Some(global_start.unwrap_or(loc_end)), ty)? - } else { - self.chunk_at(using.loc.start(), Some(global_start.unwrap_or(loc_end)), None, "*") - }; - let global_chunk = using - .global - .as_mut() - .map(|global| self.visit_to_chunk(global.loc.start(), Some(using.loc.end()), global)) - .transpose()?; - - let write_for_def = |fmt: &mut Self| { - fmt.grouped(|fmt| { - fmt.write_chunk(&for_chunk)?; - fmt.write_chunk(&ty_chunk)?; - if let Some(global_chunk) = global_chunk.as_ref() { - fmt.write_chunk(global_chunk)?; - } - Ok(()) - })?; - Ok(()) - }; - - let simulated_for_def = self.simulate_to_string(write_for_def)?; - - if is_library { - let chunk = list_chunks.pop().unwrap(); - if self.will_chunk_fit(&format!("{{}} {simulated_for_def};"), &chunk)? { - self.write_chunk(&chunk)?; - } else { - self.write_whitespace_separator(true)?; - self.grouped(|fmt| { - fmt.write_chunk(&chunk)?; - Ok(()) - })?; - self.write_whitespace_separator(true)?; - } - } else { - self.surrounded( - SurroundingChunk::new("{", Some(using.loc.start()), None), - SurroundingChunk::new( - "}", - None, - Some(ty_start.or(global_start).unwrap_or(loc_end)), - ), - |fmt, _multiline| { - let multiline = fmt.are_chunks_separated_multiline( - &format!("{{ {{}} }} {simulated_for_def};"), - &list_chunks, - ",", - )?; - fmt.write_chunks_separated(&list_chunks, ",", multiline)?; - Ok(()) - }, - )?; - } - write_for_def(self)?; - - self.write_semicolon()?; - - Ok(()) - } - - #[instrument(name = "yul_block", skip_all)] - fn visit_yul_block( - &mut self, - loc: Loc, - statements: &mut Vec, - attempt_single_line: bool, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - self.visit_block(loc, statements, attempt_single_line, false)?; - Ok(()) - } - - #[instrument(name = "yul_expr", skip_all)] - fn visit_yul_expr(&mut self, expr: &mut YulExpression) -> Result<(), Self::Error> { - return_source_if_disabled!(self, expr.loc()); - - match expr { - YulExpression::BoolLiteral(loc, val, ident) => { - let val = if *val { "true" } else { "false" }; - self.visit_yul_string_with_ident(*loc, val, ident) - } - YulExpression::FunctionCall(expr) => self.visit_yul_function_call(expr), - YulExpression::HexNumberLiteral(loc, val, ident) => { - self.visit_yul_string_with_ident(*loc, val, ident) - } - YulExpression::HexStringLiteral(val, ident) => self.visit_yul_string_with_ident( - val.loc, - &self.quote_str(val.loc, Some("hex"), &val.hex), - ident, - ), - YulExpression::NumberLiteral(loc, val, expr, ident) => { - let val = if expr.is_empty() { val.to_owned() } else { format!("{val}e{expr}") }; - self.visit_yul_string_with_ident(*loc, &val, ident) - } - YulExpression::StringLiteral(val, ident) => self.visit_yul_string_with_ident( - val.loc, - &self.quote_str(val.loc, None, &val.string), - ident, - ), - YulExpression::SuffixAccess(_, expr, ident) => { - self.visit_member_access(expr, ident, |fmt, expr| match expr.as_mut() { - YulExpression::SuffixAccess(_, inner_expr, inner_ident) => { - Ok(Some((inner_expr, inner_ident))) - } - expr => { - expr.visit(fmt)?; - Ok(None) - } - }) - } - YulExpression::Variable(ident) => { - write_chunk!(self, ident.loc.start(), ident.loc.end(), "{}", ident.name) - } - } - } - - #[instrument(name = "yul_assignment", skip_all)] - fn visit_yul_assignment( - &mut self, - loc: Loc, - exprs: &mut Vec, - expr: &mut Option<&mut YulExpression>, - ) -> Result<(), Self::Error> - where - T: Visitable + CodeLocation, - { - return_source_if_disabled!(self, loc); - - self.grouped(|fmt| { - let chunks = - fmt.items_to_chunks(None, exprs.iter_mut().map(|expr| (expr.loc(), expr)))?; - - let multiline = fmt.are_chunks_separated_multiline("{} := ", &chunks, ",")?; - fmt.write_chunks_separated(&chunks, ",", multiline)?; - - if let Some(expr) = expr { - write_chunk!(fmt, expr.loc().start(), ":=")?; - let chunk = fmt.visit_to_chunk(expr.loc().start(), Some(loc.end()), expr)?; - if !fmt.will_chunk_fit("{}", &chunk)? { - fmt.write_whitespace_separator(true)?; - } - fmt.write_chunk(&chunk)?; - } - Ok(()) - })?; - Ok(()) - } - - #[instrument(name = "yul_for", skip_all)] - fn visit_yul_for(&mut self, stmt: &mut YulFor) -> Result<(), Self::Error> { - return_source_if_disabled!(self, stmt.loc); - write_chunk!(self, stmt.loc.start(), "for")?; - self.visit_yul_block(stmt.init_block.loc, &mut stmt.init_block.statements, true)?; - stmt.condition.visit(self)?; - self.visit_yul_block(stmt.post_block.loc, &mut stmt.post_block.statements, true)?; - self.visit_yul_block(stmt.execution_block.loc, &mut stmt.execution_block.statements, true)?; - Ok(()) - } - - #[instrument(name = "yul_function_call", skip_all)] - fn visit_yul_function_call(&mut self, stmt: &mut YulFunctionCall) -> Result<(), Self::Error> { - return_source_if_disabled!(self, stmt.loc); - write_chunk!(self, stmt.loc.start(), "{}", stmt.id.name)?; - self.visit_list("", &mut stmt.arguments, None, Some(stmt.loc.end()), true) - } - - #[instrument(name = "yul_fun_def", skip_all)] - fn visit_yul_fun_def(&mut self, stmt: &mut YulFunctionDefinition) -> Result<(), Self::Error> { - return_source_if_disabled!(self, stmt.loc); - - write_chunk!(self, stmt.loc.start(), "function {}", stmt.id.name)?; - - self.visit_list("", &mut stmt.params, None, None, true)?; - - if !stmt.returns.is_empty() { - self.grouped(|fmt| { - write_chunk!(fmt, "->")?; - - let chunks = fmt.items_to_chunks( - Some(stmt.body.loc.start()), - stmt.returns.iter_mut().map(|param| (param.loc, param)), - )?; - let multiline = fmt.are_chunks_separated_multiline("{}", &chunks, ",")?; - fmt.write_chunks_separated(&chunks, ",", multiline)?; - if multiline { - fmt.write_whitespace_separator(true)?; - } - Ok(()) - })?; - } - - stmt.body.visit(self)?; - - Ok(()) - } - - #[instrument(name = "yul_if", skip_all)] - fn visit_yul_if( - &mut self, - loc: Loc, - expr: &mut YulExpression, - block: &mut YulBlock, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - write_chunk!(self, loc.start(), "if")?; - expr.visit(self)?; - self.visit_yul_block(block.loc, &mut block.statements, true) - } - - #[instrument(name = "yul_leave", skip_all)] - fn visit_yul_leave(&mut self, loc: Loc) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - write_chunk!(self, loc.start(), loc.end(), "leave") - } - - #[instrument(name = "yul_switch", skip_all)] - fn visit_yul_switch(&mut self, stmt: &mut YulSwitch) -> Result<(), Self::Error> { - return_source_if_disabled!(self, stmt.loc); - - write_chunk!(self, stmt.loc.start(), "switch")?; - stmt.condition.visit(self)?; - writeln_chunk!(self)?; - let mut cases = stmt.cases.iter_mut().peekable(); - while let Some(YulSwitchOptions::Case(loc, expr, block)) = cases.next() { - write_chunk!(self, loc.start(), "case")?; - expr.visit(self)?; - self.visit_yul_block(block.loc, &mut block.statements, true)?; - let is_last = cases.peek().is_none(); - if !is_last || stmt.default.is_some() { - writeln_chunk!(self)?; - } - } - if let Some(YulSwitchOptions::Default(loc, ref mut block)) = stmt.default { - write_chunk!(self, loc.start(), "default")?; - self.visit_yul_block(block.loc, &mut block.statements, true)?; - } - Ok(()) - } - - #[instrument(name = "yul_var_declaration", skip_all)] - fn visit_yul_var_declaration( - &mut self, - loc: Loc, - idents: &mut Vec, - expr: &mut Option, - ) -> Result<(), Self::Error> { - return_source_if_disabled!(self, loc); - self.grouped(|fmt| { - write_chunk!(fmt, loc.start(), "let")?; - fmt.visit_yul_assignment(loc, idents, &mut expr.as_mut()) - })?; - Ok(()) - } - - #[instrument(name = "yul_typed_ident", skip_all)] - fn visit_yul_typed_ident(&mut self, ident: &mut YulTypedIdentifier) -> Result<(), Self::Error> { - return_source_if_disabled!(self, ident.loc); - self.visit_yul_string_with_ident(ident.loc, &ident.id.name, &mut ident.ty) - } - - #[instrument(name = "parser_error", skip_all)] - fn visit_parser_error(&mut self, loc: Loc) -> Result<()> { - Err(FormatterError::InvalidParsedItem(loc)) - } -} - -/// An action which may be committed to a Formatter -struct Transaction<'f, 'a, W> { - fmt: &'f mut Formatter<'a, W>, - buffer: String, - comments: Comments, -} - -impl<'a, W> std::ops::Deref for Transaction<'_, 'a, W> { - type Target = Formatter<'a, W>; - fn deref(&self) -> &Self::Target { - self.fmt - } -} - -impl std::ops::DerefMut for Transaction<'_, '_, W> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.fmt - } -} - -impl<'f, 'a, W: Write> Transaction<'f, 'a, W> { - /// Create a new transaction from a callback - fn new( - fmt: &'f mut Formatter<'a, W>, - fun: impl FnMut(&mut Formatter<'a, W>) -> Result<()>, - ) -> Result { - let mut comments = fmt.comments.clone(); - let buffer = fmt.with_temp_buf(fun)?.w; - comments = std::mem::replace(&mut fmt.comments, comments); - Ok(Self { fmt, buffer, comments }) - } - - /// Commit the transaction to the Formatter - fn commit(self) -> Result { - self.fmt.comments = self.comments; - write_chunk!(self.fmt, "{}", self.buffer)?; - Ok(self.buffer) - } -} diff --git a/crates/fmt/src/helpers.rs b/crates/fmt/src/helpers.rs deleted file mode 100644 index 9d28cebd5ca5b..0000000000000 --- a/crates/fmt/src/helpers.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::{ - Comments, Formatter, FormatterConfig, FormatterError, Visitable, - inline_config::{InlineConfig, InvalidInlineConfigItem}, -}; -use ariadne::{Color, Fmt, Label, Report, ReportKind, Source}; -use itertools::Itertools; -use solang_parser::{diagnostics::Diagnostic, pt::*}; -use std::{fmt::Write, path::Path}; - -/// Result of parsing the source code -#[derive(Debug)] -pub struct Parsed<'a> { - /// The original source code. - pub src: &'a str, - /// The parse tree. - pub pt: SourceUnit, - /// Parsed comments. - pub comments: Comments, - /// Parsed inline config. - pub inline_config: InlineConfig, - /// Invalid inline config items parsed. - pub invalid_inline_config_items: Vec<(Loc, InvalidInlineConfigItem)>, -} - -/// Parse source code. -pub fn parse(src: &str) -> Result, FormatterError> { - parse_raw(src).map_err(|diag| FormatterError::Parse(src.to_string(), None, diag)) -} - -/// Parse source code with a path for diagnostics. -pub fn parse2<'s>(src: &'s str, path: Option<&Path>) -> Result, FormatterError> { - parse_raw(src) - .map_err(|diag| FormatterError::Parse(src.to_string(), path.map(ToOwned::to_owned), diag)) -} - -/// Parse source code, returning a list of diagnostics on failure. -pub fn parse_raw(src: &str) -> Result, Vec> { - let (pt, comments) = solang_parser::parse(src, 0)?; - let comments = Comments::new(comments, src); - let (inline_config_items, invalid_inline_config_items): (Vec<_>, Vec<_>) = - comments.parse_inline_config_items().partition_result(); - let inline_config = InlineConfig::new(inline_config_items, src); - Ok(Parsed { src, pt, comments, inline_config, invalid_inline_config_items }) -} - -/// Format parsed code -pub fn format_to( - writer: W, - mut parsed: Parsed<'_>, - config: FormatterConfig, -) -> Result<(), FormatterError> { - trace!(?parsed, ?config, "Formatting"); - let mut formatter = - Formatter::new(writer, parsed.src, parsed.comments, parsed.inline_config, config); - parsed.pt.visit(&mut formatter) -} - -/// Parse and format a string with default settings -pub fn format(src: &str) -> Result { - let parsed = parse(src)?; - - let mut output = String::new(); - format_to(&mut output, parsed, FormatterConfig::default())?; - - Ok(output) -} - -/// Converts the start offset of a `Loc` to `(line, col)` -pub fn offset_to_line_column(content: &str, start: usize) -> (usize, usize) { - debug_assert!(content.len() > start); - - // first line is `1` - let mut line_counter = 1; - for (offset, c) in content.chars().enumerate() { - if c == '\n' { - line_counter += 1; - } - if offset > start { - return (line_counter, offset - start); - } - } - - unreachable!("content.len() > start") -} - -/// Formats parser diagnostics -pub fn format_diagnostics_report( - content: &str, - path: Option<&Path>, - diagnostics: &[Diagnostic], -) -> String { - if diagnostics.is_empty() { - return String::new(); - } - - let filename = - path.map(|p| p.file_name().unwrap().to_string_lossy().to_string()).unwrap_or_default(); - let mut s = Vec::new(); - for diag in diagnostics { - let span = (filename.as_str(), diag.loc.start()..diag.loc.end()); - let mut report = Report::build(ReportKind::Error, span.clone()) - .with_message(format!("{:?}", diag.ty)) - .with_label( - Label::new(span) - .with_color(Color::Red) - .with_message(diag.message.as_str().fg(Color::Red)), - ); - - for note in &diag.notes { - report = report.with_note(¬e.message); - } - - report.finish().write((filename.as_str(), Source::from(content)), &mut s).unwrap(); - } - String::from_utf8(s).unwrap() -} - -pub fn import_path_string(path: &ImportPath) -> String { - match path { - ImportPath::Filename(s) => s.string.clone(), - ImportPath::Path(p) => p.to_string(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // - #[test] - fn test_interface_format() { - let s = "interface I {\n function increment() external;\n function number() external view returns (uint256);\n function setNumber(uint256 newNumber) external;\n}"; - let _formatted = format(s).unwrap(); - } -} diff --git a/crates/fmt/src/inline_config.rs b/crates/fmt/src/inline_config.rs deleted file mode 100644 index e3177efcb5034..0000000000000 --- a/crates/fmt/src/inline_config.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::comments::{CommentState, CommentStringExt}; -use itertools::Itertools; -use solang_parser::pt::Loc; -use std::{fmt, str::FromStr}; - -/// An inline config item -#[derive(Clone, Copy, Debug)] -pub enum InlineConfigItem { - /// Disables the next code item regardless of newlines - DisableNextItem, - /// Disables formatting on the current line - DisableLine, - /// Disables formatting between the next newline and the newline after - DisableNextLine, - /// Disables formatting for any code that follows this and before the next "disable-end" - DisableStart, - /// Disables formatting for any code that precedes this and after the previous "disable-start" - DisableEnd, -} - -impl FromStr for InlineConfigItem { - type Err = InvalidInlineConfigItem; - fn from_str(s: &str) -> Result { - Ok(match s { - "disable-next-item" => Self::DisableNextItem, - "disable-line" => Self::DisableLine, - "disable-next-line" => Self::DisableNextLine, - "disable-start" => Self::DisableStart, - "disable-end" => Self::DisableEnd, - s => return Err(InvalidInlineConfigItem(s.into())), - }) - } -} - -#[derive(Debug)] -pub struct InvalidInlineConfigItem(String); - -impl fmt::Display for InvalidInlineConfigItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_fmt(format_args!("Invalid inline config item: {}", self.0)) - } -} - -/// A disabled formatting range. `loose` designates that the range includes any loc which -/// may start in between start and end, whereas the strict version requires that -/// `range.start >= loc.start <=> loc.end <= range.end` -#[derive(Debug)] -struct DisabledRange { - start: usize, - end: usize, - loose: bool, -} - -impl DisabledRange { - fn includes(&self, loc: Loc) -> bool { - loc.start() >= self.start && (if self.loose { loc.start() } else { loc.end() } <= self.end) - } -} - -/// An inline config. Keeps track of disabled ranges. -/// -/// This is a list of Inline Config items for locations in a source file. This is -/// usually acquired by parsing the comments for an `forgefmt:` items. -/// -/// See [`Comments::parse_inline_config_items`](crate::Comments::parse_inline_config_items) for -/// details. -#[derive(Debug, Default)] -pub struct InlineConfig { - disabled_ranges: Vec, -} - -impl InlineConfig { - /// Build a new inline config with an iterator of inline config items and their locations in a - /// source file - pub fn new(items: impl IntoIterator, src: &str) -> Self { - let mut disabled_ranges = vec![]; - let mut disabled_range_start = None; - let mut disabled_depth = 0usize; - for (loc, item) in items.into_iter().sorted_by_key(|(loc, _)| loc.start()) { - match item { - InlineConfigItem::DisableNextItem => { - let offset = loc.end(); - let mut char_indices = src[offset..] - .comment_state_char_indices() - .filter_map(|(state, idx, ch)| match state { - CommentState::None => Some((idx, ch)), - _ => None, - }) - .skip_while(|(_, ch)| ch.is_whitespace()); - if let Some((mut start, _)) = char_indices.next() { - start += offset; - let end = char_indices - .find(|(_, ch)| !ch.is_whitespace()) - .map(|(idx, _)| offset + idx) - .unwrap_or(src.len()); - disabled_ranges.push(DisabledRange { start, end, loose: true }); - } - } - InlineConfigItem::DisableLine => { - let mut prev_newline = - src[..loc.start()].char_indices().rev().skip_while(|(_, ch)| *ch != '\n'); - let start = prev_newline - .next() - .map(|(idx, _)| { - if let Some((idx, ch)) = prev_newline.next() { - match ch { - '\r' => idx, - _ => idx + 1, - } - } else { - idx - } - }) - .unwrap_or_default(); - - let end_offset = loc.end(); - let mut next_newline = - src[end_offset..].char_indices().skip_while(|(_, ch)| *ch != '\n'); - let end = - end_offset + next_newline.next().map(|(idx, _)| idx).unwrap_or_default(); - - disabled_ranges.push(DisabledRange { start, end, loose: false }); - } - InlineConfigItem::DisableNextLine => { - let offset = loc.end(); - let mut char_indices = - src[offset..].char_indices().skip_while(|(_, ch)| *ch != '\n').skip(1); - if let Some((mut start, _)) = char_indices.next() { - start += offset; - let end = char_indices - .find(|(_, ch)| *ch == '\n') - .map(|(idx, _)| offset + idx + 1) - .unwrap_or(src.len()); - disabled_ranges.push(DisabledRange { start, end, loose: false }); - } - } - InlineConfigItem::DisableStart => { - if disabled_depth == 0 { - disabled_range_start = Some(loc.end()); - } - disabled_depth += 1; - } - InlineConfigItem::DisableEnd => { - disabled_depth = disabled_depth.saturating_sub(1); - if disabled_depth == 0 - && let Some(start) = disabled_range_start.take() - { - disabled_ranges.push(DisabledRange { - start, - end: loc.start(), - loose: false, - }) - } - } - } - } - if let Some(start) = disabled_range_start.take() { - disabled_ranges.push(DisabledRange { start, end: src.len(), loose: false }) - } - Self { disabled_ranges } - } - - /// Check if the location is in a disabled range - pub fn is_disabled(&self, loc: Loc) -> bool { - self.disabled_ranges.iter().any(|range| range.includes(loc)) - } -} diff --git a/crates/fmt/src/lib.rs b/crates/fmt/src/lib.rs index 0ad3e336f7af2..5830f879e67c7 100644 --- a/crates/fmt/src/lib.rs +++ b/crates/fmt/src/lib.rs @@ -1,27 +1,302 @@ #![doc = include_str!("../README.md")] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![allow(dead_code)] // TODO(dani) -#[macro_use] -extern crate tracing; +const DEBUG: bool = false || option_env!("FMT_DEBUG").is_some(); +const DEBUG_INDENT: bool = false; -mod buffer; -pub mod chunk; -mod comments; -mod formatter; -mod helpers; -pub mod inline_config; -mod macros; -pub mod solang_ext; -mod string; -pub mod visit; +use foundry_common::comments::{ + Comment, Comments, + inline_config::{InlineConfig, InlineConfigItem}, +}; -pub use foundry_config::fmt::*; +// TODO(dani) +// #[macro_use] +// extern crate tracing; +use tracing as _; +use tracing_subscriber as _; + +mod state; -pub use comments::Comments; -pub use formatter::{Formatter, FormatterError}; -pub use helpers::{ - Parsed, format, format_diagnostics_report, format_to, offset_to_line_column, parse, parse2, +mod pp; + +use solar::{ + parse::{ + ast::{SourceUnit, Span}, + interface::{Session, diagnostics::EmittedDiagnostics, source_map::SourceFile}, + }, + sema::{Compiler, Gcx, Source}, }; -pub use inline_config::InlineConfig; -pub use visit::{Visitable, Visitor}; + +use std::{path::Path, sync::Arc}; + +pub use foundry_config::fmt::*; + +/// The result of the formatter. +pub type FormatterResult = DiagnosticsResult; + +/// The result of the formatter. +#[derive(Debug)] +pub enum DiagnosticsResult { + /// Everything went well. + Ok(T), + /// No errors encountered, but warnings or other non-error diagnostics were emitted. + OkWithDiagnostics(T, E), + /// Errors encountered, but a result was produced anyway. + ErrRecovered(T, E), + /// Fatal errors encountered. + Err(E), +} + +impl DiagnosticsResult { + /// Converts the formatter result into a standard result. + /// + /// This ignores any non-error diagnostics if `Ok`, and any valid result if `Err`. + pub fn into_result(self) -> Result { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) => Ok(s), + Self::ErrRecovered(_, d) | Self::Err(d) => Err(d), + } + } + + /// Returns the result, even if it was produced with errors. + pub fn into_ok(self) -> Result { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Ok(s), + Self::Err(e) => Err(e), + } + } + + /// Returns any result produced. + pub fn ok_ref(&self) -> Option<&T> { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Some(s), + Self::Err(_) => None, + } + } + + /// Returns any diagnostics emitted. + pub fn err_ref(&self) -> Option<&E> { + match self { + Self::Ok(_) => None, + Self::OkWithDiagnostics(_, d) | Self::ErrRecovered(_, d) | Self::Err(d) => Some(d), + } + } + + /// Returns `true` if the result is `Ok`. + pub fn is_ok(&self) -> bool { + matches!(self, Self::Ok(_) | Self::OkWithDiagnostics(_, _)) + } + + /// Returns `true` if the result is `Err`. + pub fn is_err(&self) -> bool { + !self.is_ok() + } +} + +pub fn format_file( + path: &Path, + config: Arc, + compiler: &mut Compiler, +) -> FormatterResult { + format_inner(config, compiler, &|sess| { + sess.source_map().load_file(path).map_err(|e| sess.dcx.err(e.to_string()).emit()) + }) +} + +pub fn format_source( + source: &str, + path: Option<&Path>, + config: Arc, + compiler: &mut Compiler, +) -> FormatterResult { + format_inner(config, compiler, &|sess| { + let name = match path { + Some(path) => solar::parse::interface::source_map::FileName::Real(path.to_path_buf()), + None => solar::parse::interface::source_map::FileName::Stdin, + }; + sess.source_map() + .new_source_file(name, source) + .map_err(|e| sess.dcx.err(e.to_string()).emit()) + }) +} + +/// Format a string input with the default compiler. +pub fn format(source: &str, config: FormatterConfig) -> FormatterResult { + let mut compiler = Compiler::new( + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(), + ); + + format_source(source, None, Arc::new(config), &mut compiler) +} + +fn format_inner( + config: Arc, + compiler: &mut Compiler, + mk_file: &(dyn Fn(&Session) -> solar::parse::interface::Result> + Sync + Send), +) -> FormatterResult { + // First pass formatting + let first_result = format_once(config.clone(), compiler, mk_file); + + // If first pass was not successful, return the result + if first_result.is_err() { + return first_result; + } + let Some(first_formatted) = first_result.ok_ref() else { return first_result }; + + // Second pass formatting + let second_result = format_once(config, compiler, &|sess| { + sess.source_map() + .new_source_file( + solar::parse::interface::source_map::FileName::Custom("format-again".to_string()), + first_formatted, + ) + .map_err(|e| sess.dcx.err(e.to_string()).emit()) + }); + + // Check if the two passes produce the same output (idempotency) + match (first_result.ok_ref(), second_result.ok_ref()) { + (Some(first), Some(second)) if first != second => { + panic!("formatter is not idempotent:\n{}", diff(first, second)); + } + _ => {} + } + + if first_result.is_ok() && second_result.is_err() && !DEBUG { + panic!( + "failed to format a second time:\nfirst_result={first_result:#?}\nsecond_result={second_result:#?}" + ); + // second_result + } else { + first_result + } +} + +fn diff(first: &str, second: &str) -> impl std::fmt::Display { + use std::fmt::Write; + let diff = similar::TextDiff::from_lines(first, second); + let mut s = String::new(); + for change in diff.iter_all_changes() { + let tag = match change.tag() { + similar::ChangeTag::Delete => "-", + similar::ChangeTag::Insert => "+", + similar::ChangeTag::Equal => " ", + }; + write!(s, "{tag}{change}").unwrap(); + } + s +} + +fn format_once( + config: Arc, + compiler: &mut Compiler, + mk_file: &( + dyn Fn(&solar::interface::Session) -> solar::interface::Result> + + Send + + Sync + ), +) -> FormatterResult { + let res = compiler.enter_mut(|c| -> solar::interface::Result { + let mut pcx = c.parse(); + pcx.set_resolve_imports(false); + + let file = mk_file(c.sess())?; + let file_path = file.name.as_real(); + + if let Some(path) = file_path { + pcx.load_files(&[path.to_path_buf()])?; + } else { + // Fallback for non-file sources like stdin + pcx.add_file(file.to_owned()); + } + pcx.parse(); + + let gcx = c.gcx(); + // Iterate over `gcx.sources` to find the correct `SourceUnit` + let source = if let Some(path) = file_path { + gcx.sources.iter().find(|su| su.file.name.as_real() == Some(path)) + } else { + gcx.sources.first() + } + .ok_or_else(|| c.dcx().bug("no source file parsed").emit())?; + + Ok(format_ast(&gcx, source, config)) + }); + + let diagnostics = compiler.sess().dcx.emitted_diagnostics().unwrap(); + match (res, compiler.sess().dcx.has_errors()) { + (Ok(s), Ok(())) if diagnostics.is_empty() => FormatterResult::Ok(s), + (Ok(s), Ok(())) => FormatterResult::OkWithDiagnostics(s, diagnostics), + (Ok(s), Err(_)) => FormatterResult::ErrRecovered(s, diagnostics), + (Err(_), Ok(_)) => unreachable!(), + (Err(_), Err(_)) => FormatterResult::Err(diagnostics), + } +} + +// A parallel-safe "worker" function. +pub fn format_ast<'ast>( + gcx: &Gcx<'ast>, + source: &'ast Source<'ast>, + config: Arc, +) -> String { + let comments = Comments::new( + &source.file, + gcx.sess.source_map(), + true, + config.wrap_comments, + if matches!(config.style, IndentStyle::Tab) { Some(config.tab_width) } else { None }, + ); + let inline_config = parse_inline_config( + gcx.sess, + &comments, + source.ast.as_ref().expect("unable to get the AST"), + ); + + let mut state = state::State::new(gcx.sess.source_map(), config, inline_config, comments); + state.print_source_unit(source.ast.as_ref().expect("unable to get the AST")); + state.s.eof() +} + +fn parse_inline_config<'ast>( + sess: &Session, + comments: &Comments, + ast: &'ast SourceUnit<'ast>, +) -> InlineConfig<()> { + let parse_item = |mut item: &str, cmnt: &Comment| -> Option<(Span, InlineConfigItem<()>)> { + if let Some(prefix) = cmnt.prefix() { + item = item.strip_prefix(prefix).unwrap_or(item); + } + if let Some(suffix) = cmnt.suffix() { + item = item.strip_suffix(suffix).unwrap_or(item); + } + let item = item.trim_start().strip_prefix("forgefmt:")?.trim(); + match item.parse::>() { + Ok(item) => Some((cmnt.span, item)), + Err(e) => { + sess.dcx.warn(e.to_string()).span(cmnt.span).emit(); + None + } + } + }; + + let items = comments.iter().flat_map(|cmnt| { + let mut found_items = Vec::with_capacity(2); + // Always process the first line. + if let Some(line) = cmnt.lines.first() + && let Some(item) = parse_item(line, cmnt) + { + found_items.push(item); + } + // If the comment has more than one line, process the last line. + if cmnt.lines.len() > 1 + && let Some(line) = cmnt.lines.last() + && let Some(item) = parse_item(line, cmnt) + { + found_items.push(item); + } + found_items + }); + + InlineConfig::from_ast(items, ast, sess.source_map()) +} diff --git a/crates/fmt/src/macros.rs b/crates/fmt/src/macros.rs deleted file mode 100644 index 68305d9f2f11d..0000000000000 --- a/crates/fmt/src/macros.rs +++ /dev/null @@ -1,125 +0,0 @@ -macro_rules! write_chunk { - ($self:expr, $format_str:literal) => {{ - write_chunk!($self, $format_str,) - }}; - ($self:expr, $format_str:literal, $($arg:tt)*) => {{ - $self.write_chunk(&format!($format_str, $($arg)*).into()) - }}; - ($self:expr, $loc:expr) => {{ - write_chunk!($self, $loc, "") - }}; - ($self:expr, $loc:expr, $format_str:literal) => {{ - write_chunk!($self, $loc, $format_str,) - }}; - ($self:expr, $loc:expr, $format_str:literal, $($arg:tt)*) => {{ - let chunk = $self.chunk_at($loc, None, None, format_args!($format_str, $($arg)*),); - $self.write_chunk(&chunk) - }}; - ($self:expr, $loc:expr, $end_loc:expr, $format_str:literal) => {{ - write_chunk!($self, $loc, $end_loc, $format_str,) - }}; - ($self:expr, $loc:expr, $end_loc:expr, $format_str:literal, $($arg:tt)*) => {{ - let chunk = $self.chunk_at($loc, Some($end_loc), None, format_args!($format_str, $($arg)*),); - $self.write_chunk(&chunk) - }}; - ($self:expr, $loc:expr, $end_loc:expr, $needs_space:expr, $format_str:literal, $($arg:tt)*) => {{ - let chunk = $self.chunk_at($loc, Some($end_loc), Some($needs_space), format_args!($format_str, $($arg)*),); - $self.write_chunk(&chunk) - }}; -} - -macro_rules! writeln_chunk { - ($self:expr) => {{ - writeln_chunk!($self, "") - }}; - ($self:expr, $format_str:literal) => {{ - writeln_chunk!($self, $format_str,) - }}; - ($self:expr, $format_str:literal, $($arg:tt)*) => {{ - write_chunk!($self, "{}\n", format_args!($format_str, $($arg)*)) - }}; - ($self:expr, $loc:expr) => {{ - writeln_chunk!($self, $loc, "") - }}; - ($self:expr, $loc:expr, $format_str:literal) => {{ - writeln_chunk!($self, $loc, $format_str,) - }}; - ($self:expr, $loc:expr, $format_str:literal, $($arg:tt)*) => {{ - write_chunk!($self, $loc, "{}\n", format_args!($format_str, $($arg)*)) - }}; - ($self:expr, $loc:expr, $end_loc:expr, $format_str:literal) => {{ - writeln_chunk!($self, $loc, $end_loc, $format_str,) - }}; - ($self:expr, $loc:expr, $end_loc:expr, $format_str:literal, $($arg:tt)*) => {{ - write_chunk!($self, $loc, $end_loc, "{}\n", format_args!($format_str, $($arg)*)) - }}; -} - -macro_rules! write_chunk_spaced { - ($self:expr, $loc:expr, $needs_space:expr, $format_str:literal) => {{ - write_chunk_spaced!($self, $loc, $needs_space, $format_str,) - }}; - ($self:expr, $loc:expr, $needs_space:expr, $format_str:literal, $($arg:tt)*) => {{ - let chunk = $self.chunk_at($loc, None, $needs_space, format_args!($format_str, $($arg)*),); - $self.write_chunk(&chunk) - }}; -} - -macro_rules! buf_fn { - ($vis:vis fn $name:ident(&self $(,)? $($arg_name:ident : $arg_ty:ty),*) $(-> $ret:ty)?) => { - $vis fn $name(&self, $($arg_name : $arg_ty),*) $(-> $ret)? { - if self.temp_bufs.is_empty() { - self.buf.$name($($arg_name),*) - } else { - self.temp_bufs.last().unwrap().$name($($arg_name),*) - } - } - }; - ($vis:vis fn $name:ident(&mut self $(,)? $($arg_name:ident : $arg_ty:ty),*) $(-> $ret:ty)?) => { - $vis fn $name(&mut self, $($arg_name : $arg_ty),*) $(-> $ret)? { - if self.temp_bufs.is_empty() { - self.buf.$name($($arg_name),*) - } else { - self.temp_bufs.last_mut().unwrap().$name($($arg_name),*) - } - } - }; -} - -macro_rules! return_source_if_disabled { - ($self:expr, $loc:expr) => {{ - let loc = $loc; - if $self.inline_config.is_disabled(loc) { - trace!("Returning because disabled: {loc:?}"); - return $self.visit_source(loc); - } - }}; - ($self:expr, $loc:expr, $suffix:literal) => {{ - let mut loc = $loc; - let has_suffix = $self.extend_loc_until(&mut loc, $suffix); - if $self.inline_config.is_disabled(loc) { - $self.visit_source(loc)?; - trace!("Returning because disabled: {loc:?}"); - if !has_suffix { - write!($self.buf(), "{}", $suffix)?; - } - return Ok(()); - } - }}; -} - -macro_rules! visit_source_if_disabled_else { - ($self:expr, $loc:expr, $block:block) => {{ - let loc = $loc; - if $self.inline_config.is_disabled(loc) { - $self.visit_source(loc)?; - } else $block - }}; -} - -pub(crate) use buf_fn; -pub(crate) use return_source_if_disabled; -pub(crate) use visit_source_if_disabled_else; -pub(crate) use write_chunk; -pub(crate) use write_chunk_spaced; -pub(crate) use writeln_chunk; diff --git a/crates/fmt-2/src/main.rs b/crates/fmt/src/main.rs similarity index 90% rename from crates/fmt-2/src/main.rs rename to crates/fmt/src/main.rs index d10c31a6dd61d..791d3cbd35322 100644 --- a/crates/fmt-2/src/main.rs +++ b/crates/fmt/src/main.rs @@ -2,7 +2,7 @@ #![allow(dead_code, clippy::disallowed_macros)] -use std::{io::Read, path::PathBuf}; +use std::{io::Read, path::PathBuf, sync::Arc}; use foundry_common::compile::ProjectCompiler; @@ -31,7 +31,7 @@ fn main() { }; let compiler = output.parser_mut().solc_mut().compiler_mut(); - let result = forge_fmt_2::format_source(&src, path.as_deref(), config.fmt, compiler); + let result = forge_fmt::format_source(&src, path.as_deref(), Arc::new(config.fmt), compiler); if let Some(formatted) = result.ok_ref() { print!("{formatted}"); } diff --git a/crates/fmt-2/src/pp/convenience.rs b/crates/fmt/src/pp/convenience.rs similarity index 100% rename from crates/fmt-2/src/pp/convenience.rs rename to crates/fmt/src/pp/convenience.rs diff --git a/crates/fmt-2/src/pp/helpers.rs b/crates/fmt/src/pp/helpers.rs similarity index 100% rename from crates/fmt-2/src/pp/helpers.rs rename to crates/fmt/src/pp/helpers.rs diff --git a/crates/fmt-2/src/pp/mod.rs b/crates/fmt/src/pp/mod.rs similarity index 100% rename from crates/fmt-2/src/pp/mod.rs rename to crates/fmt/src/pp/mod.rs diff --git a/crates/fmt-2/src/pp/ring.rs b/crates/fmt/src/pp/ring.rs similarity index 100% rename from crates/fmt-2/src/pp/ring.rs rename to crates/fmt/src/pp/ring.rs diff --git a/crates/fmt-2/src/state/common.rs b/crates/fmt/src/state/common.rs similarity index 97% rename from crates/fmt-2/src/state/common.rs rename to crates/fmt/src/state/common.rs index 35a8fe0c92abc..a1bcf2b289b8c 100644 --- a/crates/fmt-2/src/state/common.rs +++ b/crates/fmt/src/state/common.rs @@ -10,6 +10,17 @@ use solar::parse::{ }; use std::{borrow::Cow, fmt::Debug}; +pub(crate) trait LitExt<'ast> { + fn is_str_concatenation(&self) -> bool; +} + +impl<'ast> LitExt<'ast> for ast::Lit<'ast> { + /// Checks if a the input literal is a string literal with multiple parts. + fn is_str_concatenation(&self) -> bool { + if let ast::LitKind::Str(_, _, parts) = &self.kind { !parts.is_empty() } else { false } + } +} + /// Language-specific pretty printing. Common for both: Solidity + Yul. impl<'ast> State<'_, 'ast> { pub(super) fn print_lit(&mut self, lit: &'ast ast::Lit<'ast>) { @@ -20,9 +31,8 @@ impl<'ast> State<'_, 'ast> { match *kind { ast::LitKind::Str(kind, ..) => { - self.cbox(0); + self.s.ibox(0); for (pos, (span, symbol)) in lit.literals().delimited() { - self.ibox(0); if !self.handle_span(span, false) { let quote_pos = span.lo() + kind.prefix().len() as u32; self.print_str_lit(kind, quote_pos, symbol.as_str()); @@ -34,7 +44,6 @@ impl<'ast> State<'_, 'ast> { } else { self.neverbreak(); } - self.end(); } self.end(); } @@ -77,8 +86,13 @@ impl<'ast> State<'_, 'ast> { debug_assert!(source.is_ascii(), "{source:?}"); let config = self.config.number_underscore; + let is_dec = !["0x", "0b", "0o"].iter().any(|prefix| source.starts_with(prefix)); - let (val, exp) = source.split_once(['e', 'E']).unwrap_or((source, "")); + let (val, exp) = if !is_dec { + (source, "") + } else { + source.split_once(['e', 'E']).unwrap_or((source, "")) + }; let (val, fract) = val.split_once('.').unwrap_or((val, "")); let strip_underscores = !config.is_preserve(); @@ -88,7 +102,7 @@ impl<'ast> State<'_, 'ast> { // strip any padded 0's let mut exp_sign = ""; - if !["0x", "0b", "0o"].iter().any(|prefix| source.starts_with(prefix)) { + if is_dec { val = val.trim_start_matches('0'); fract = fract.trim_end_matches('0'); (exp_sign, exp) = diff --git a/crates/fmt-2/src/state/mod.rs b/crates/fmt/src/state/mod.rs similarity index 97% rename from crates/fmt-2/src/state/mod.rs rename to crates/fmt/src/state/mod.rs index 31dc7adf87630..aacb8705b29e5 100644 --- a/crates/fmt-2/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -13,7 +13,7 @@ use solar::parse::{ interface::{BytePos, SourceMap}, token, }; -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; mod common; mod sol; @@ -25,7 +25,7 @@ pub(super) struct State<'sess, 'ast> { sm: &'sess SourceMap, pub(super) comments: Comments, - config: FormatterConfig, + config: Arc, inline_config: InlineConfig<()>, cursor: SourcePos, @@ -35,6 +35,7 @@ pub(super) struct State<'sess, 'ast> { binary_expr: bool, member_expr: bool, var_init: bool, + fn_body: bool, } impl std::ops::Deref for State<'_, '_> { @@ -96,7 +97,7 @@ impl Separator { impl<'sess> State<'sess, '_> { pub(super) fn new( sm: &'sess SourceMap, - config: FormatterConfig, + config: Arc, inline_config: InlineConfig<()>, comments: Comments, ) -> Self { @@ -121,6 +122,7 @@ impl<'sess> State<'sess, '_> { binary_expr: false, member_expr: false, var_init: false, + fn_body: false, } } @@ -320,7 +322,8 @@ impl<'sess> State<'sess, '_> { if cmnt.style.is_blank() { match config.skip_blanks { Some(Skip::All) => continue, - Some(Skip::Leading) if is_leading => continue, + Some(Skip::Leading { resettable: true }) if is_leading => continue, + Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue, Some(Skip::Trailing) => { buffered_blank = Some(cmnt); continue; @@ -647,8 +650,9 @@ impl<'sess> State<'sess, '_> { #[derive(Clone, Copy)] enum Skip { All, - Leading, + Leading { resettable: bool }, Trailing, + LeadingNoReset, } #[derive(Default, Clone, Copy)] @@ -670,8 +674,8 @@ impl CommentConfig { Self { skip_blanks: Some(Skip::All), ..Default::default() } } - pub(crate) fn skip_leading_ws() -> Self { - Self { skip_blanks: Some(Skip::Leading), ..Default::default() } + pub(crate) fn skip_leading_ws(resettable: bool) -> Self { + Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() } } pub(crate) fn skip_trailing_ws() -> Self { diff --git a/crates/fmt-2/src/state/sol.rs b/crates/fmt/src/state/sol.rs similarity index 92% rename from crates/fmt-2/src/state/sol.rs rename to crates/fmt/src/state/sol.rs index 8496b0b12438e..35c6acd8863b6 100644 --- a/crates/fmt-2/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -1,6 +1,6 @@ #![allow(clippy::too_many_arguments)] -use crate::pp::SIZE_INFINITY; +use crate::{pp::SIZE_INFINITY, state::common::LitExt}; use foundry_common::{comments::Comment, iter::IterDelimited}; use foundry_config::fmt::{self as config, MultilineFuncHeaderStyle}; use solar::parse::{ @@ -120,7 +120,7 @@ impl<'ast> State<'_, 'ast> { } let add_zero_break = if skip_ws { - self.print_comments(span.lo(), CommentConfig::skip_leading_ws()) + self.print_comments(span.lo(), CommentConfig::skip_leading_ws(false)) } else { self.print_comments(span.lo(), CommentConfig::default()) } @@ -315,7 +315,7 @@ impl<'ast> State<'_, 'ast> { } let body_lo = body[0].span.lo(); if self.peek_comment_before(body_lo).is_some() { - self.print_comments(body_lo, CommentConfig::skip_leading_ws()); + self.print_comments(body_lo, CommentConfig::skip_leading_ws(true)); } let mut is_first = true; @@ -332,7 +332,7 @@ impl<'ast> State<'_, 'ast> { } } - if let Some(cmnt) = self.print_comments(span.hi(), CommentConfig::skip_ws()) + if let Some(cmnt) = self.print_comments(span.hi(), CommentConfig::skip_trailing_ws()) && self.config.contract_new_lines && !cmnt.is_blank() { @@ -544,6 +544,10 @@ impl<'ast> State<'_, 'ast> { // Print fn body if let Some(body) = body { + // update cache + let cache = self.fn_body; + self.fn_body = true; + if self.handle_span(self.cursor.span(body_span.lo()), false) { // Print spacing if necessary. Updates cursor. } else { @@ -564,6 +568,7 @@ impl<'ast> State<'_, 'ast> { if header.modifiers.is_empty() && header.override_.is_none() && returns.as_ref().is_none_or(|r| r.is_empty()) + && (header.visibility().is_none() || body.is_empty()) { self.nbsp(); } else { @@ -584,6 +589,9 @@ impl<'ast> State<'_, 'ast> { self.print_word("}"); self.cursor.advance_to(body_span.hi(), true); } + + // restore cache + self.fn_body = cache; } else { self.print_comments(body_span.lo(), CommentConfig::skip_ws().mixed_prev_space()); self.end(); @@ -593,6 +601,7 @@ impl<'ast> State<'_, 'ast> { self.neverbreak(); self.print_word(";"); } + self.fn_body = false; if let Some(cmnt) = self.peek_trailing_comment(body_span.hi(), None) { if cmnt.is_doc { @@ -724,7 +733,12 @@ impl<'ast> State<'_, 'ast> { // have double breaks (which should have double indentation) or not. // Alternatively, we could achieve the same behavior with a new box group that supports // "continuation" which would only increase indentation if its parent box broke. - let init_space_left = self.space_left(); + let init_space_left = std::cmp::min( + self.space_left(), + self.config.line_length + - if self.contract.is_some() { self.config.tab_width } else { 0 } + - if self.fn_body { self.config.tab_width } else { 0 }, + ); let mut pre_init_size = self.estimate_size(ty.span); // Non-elementary types use commasep which has its own padding. @@ -800,7 +814,15 @@ impl<'ast> State<'_, 'ast> { self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false); } - if is_binary_expr(&init.kind) { + if let ast::ExprKind::Lit(lit, ..) = &init.kind + && lit.is_str_concatenation() + { + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.s.ibox(self.ind); + self.print_expr(init); + self.end(); + } else if is_binary_expr(&init.kind) { if !self.is_bol_or_only_ind() { Separator::Space.print(&mut self.s, &mut self.cursor); } @@ -809,13 +831,22 @@ impl<'ast> State<'_, 'ast> { } self.print_expr(init); } else { - self.s.ibox(if pre_init_size + 3 > init_space_left { self.ind } else { 0 }); + self.s.ibox(if pre_init_size + 4 >= init_space_left { self.ind } else { 0 }); if has_complex_successor(&init.kind, true) && !matches!(&init.kind, ast::ExprKind::Member(..)) { // delegate breakpoints to `self.commasep(..)` if !self.is_bol_or_only_ind() { - self.print_sep(Separator::Nbsp); + let init_size = self.estimate_size(init.span); + if init_size + pre_init_size + 4 >= init_space_left + && init_size + 1 + self.config.tab_width < init_space_left + && !self.has_comment_between(init.span.lo(), init.span.hi()) + { + self.print_sep(Separator::Space); + self.s.offset(self.ind); + } else { + self.print_sep(Separator::Nbsp); + } } } else { if !self.is_bol_or_only_ind() { @@ -1087,13 +1118,47 @@ impl<'ast> State<'_, 'ast> { self.print_array(exprs, expr.span, |this, e| this.print_expr(e), get_span!()) } ast::ExprKind::Assign(lhs, None, rhs) => { - self.s.ibox(if has_complex_successor(&rhs.kind, false) { 0 } else { self.ind }); + let space_left = self.space_left(); + let lhs_size = self.estimate_size(lhs.span); + let rhs_size = self.estimate_size(rhs.span); + // 'lhs' + ' = ' + 'rhs' + ';' + let overflows = rhs_size + lhs_size + 4 > space_left; + let fits_alone = rhs_size <= space_left - self.config.tab_width; + + self.s.ibox( + if has_complex_successor(&rhs.kind, false) + && !(matches!(rhs.kind, ast::ExprKind::Call(..)) && overflows && fits_alone) + { + 0 + } else { + self.ind + }, + ); self.print_expr(lhs); - self.word(" = "); - self.neverbreak(); - self.print_expr(rhs); + self.word(" ="); + + if let ast::ExprKind::Lit(lit, ..) = &rhs.kind + && lit.is_str_concatenation() + { + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.s.ibox(self.ind); + self.print_expr(rhs); + self.end(); + } else if (matches!(rhs.kind, ast::ExprKind::Call(..)) && overflows && fits_alone) + || (matches!(rhs.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..)) + && overflows) + { + self.print_sep(Separator::Space); + self.print_expr(rhs); + } else { + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.print_expr(rhs); + } self.end(); } + ast::ExprKind::Assign(lhs, Some(bin_op), rhs) | ast::ExprKind::Binary(lhs, bin_op, rhs) => { let is_parent = matches!(lhs.kind, ast::ExprKind::Binary(..)) @@ -1115,7 +1180,14 @@ impl<'ast> State<'_, 'ast> { self.word(bin_op.kind.to_str()); self.word("="); } else { - if !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo())) { + if !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo())) + && self + .print_comments( + bin_op.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ) + .is_none_or(|cmnt| cmnt.is_mixed()) + { if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) { self.space_if_not_bol(); @@ -1129,7 +1201,11 @@ impl<'ast> State<'_, 'ast> { // box expressions with complex successors to accommodate their own indentation if !is_child && is_parent { if has_complex_successor(&rhs.kind, true) { - self.s.ibox(-self.ind); + if matches!(kind, ast::ExprKind::Assign(..)) { + self.s.ibox(-self.ind); + } else { + self.s.ibox(0); + } } else if has_complex_successor(&rhs.kind, false) { self.s.ibox(0); } @@ -1155,8 +1231,15 @@ impl<'ast> State<'_, 'ast> { } } ast::ExprKind::Call(expr, call_args) => { + let space_left = self.space_left(); + let expr_size = self.estimate_size(expr.span); + let args_size = self.estimate_size(call_args.span); + self.print_expr(expr); - self.print_call_args(call_args); + self.print_call_args( + call_args, + call_args.len() == 1 && args_size + 2 + expr_size > space_left, + ); } ast::ExprKind::CallOptions(expr, named_args) => { self.print_expr(expr); @@ -1275,7 +1358,7 @@ impl<'ast> State<'_, 'ast> { } ast::ExprKind::Payable(args) => { self.word("payable"); - self.print_call_args(args); + self.print_call_args(args, false); } ast::ExprKind::Ternary(cond, then, els) => { self.s.cbox(self.ind); @@ -1365,11 +1448,11 @@ impl<'ast> State<'_, 'ast> { let ast::Modifier { name, arguments } = modifier; self.print_path(name, false); if !arguments.is_empty() || add_parens_if_empty { - self.print_call_args(arguments); + self.print_call_args(arguments, false); } } - fn print_call_args(&mut self, args: &'ast ast::CallArgs<'ast>) { + fn print_call_args(&mut self, args: &'ast ast::CallArgs<'ast>, break_single_no_cmnts: bool) { let ast::CallArgs { span, ref kind } = *args; if self.handle_span(span, true) { return; @@ -1384,7 +1467,7 @@ impl<'ast> State<'_, 'ast> { |this, e| this.print_expr(e), get_span!(), ListFormat::Compact { cmnts_break: true, with_space: false }, - false, + break_single_no_cmnts, ); } ast::CallArgsKind::Named(named_args) => { @@ -1481,6 +1564,10 @@ impl<'ast> State<'_, 'ast> { } ast::StmtKind::DeclSingle(var) => self.print_var(var, true), ast::StmtKind::DeclMulti(vars, expr) => { + let space_left = self.space_left(); + + self.s.ibox(self.ind); + self.s.ibox(-self.ind); self.print_tuple( vars, span.lo(), @@ -1494,9 +1581,21 @@ impl<'ast> State<'_, 'ast> { ListFormat::Consistent { cmnts_break: false, with_space: false }, false, ); - self.word(" = "); - self.neverbreak(); + self.end(); + self.word(" ="); + if self.estimate_size(expr.span) + self.config.tab_width + <= std::cmp::max(space_left, self.space_left()) + { + self.print_sep(Separator::Space); + self.ibox(0); + } else { + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.s.ibox(-self.ind); + } self.print_expr(expr); + self.end(); + self.end(); } ast::StmtKind::Block(stmts) => self.print_block(stmts, span), ast::StmtKind::Break => self.word("break"), @@ -1652,7 +1751,20 @@ impl<'ast> State<'_, 'ast> { ); self.nbsp(); } - self.print_block(block, *try_span); + if block.is_empty() { + self.print_block(block, *try_span); + self.end(); + } else { + self.print_word("{"); + self.end(); + self.neverbreak(); + self.print_trailing_comment_no_break(try_span.lo(), None); + self.print_block_without_braces(block, try_span.hi(), Some(self.ind)); + if self.cursor.enabled || self.cursor.pos < try_span.hi() { + self.print_word("}"); + self.cursor.advance_to(try_span.hi(), true); + } + } let mut skip_ind = false; if self @@ -1663,7 +1775,6 @@ impl<'ast> State<'_, 'ast> { self.break_offset_if_not_bol(0, self.ind, false); skip_ind = true; }; - self.end(); let mut prev_block_multiline = self.is_multiline_block(block, false); @@ -1701,8 +1812,12 @@ impl<'ast> State<'_, 'ast> { } self.print_word("{"); self.end(); + self.print_trailing_comment_no_break(catch_span.lo(), None); self.print_block_without_braces(block, catch_span.hi(), Some(self.ind)); - self.print_word("}"); + if self.cursor.enabled || self.cursor.pos < try_span.hi() { + self.print_word("}"); + self.cursor.advance_to(catch_span.hi(), true); + } prev_block_multiline = current_block_multiline; } @@ -1756,7 +1871,16 @@ impl<'ast> State<'_, 'ast> { // clause: `self.print_if_cond("if", cond, cond.span.hi());` if !self.handle_span(Span::new(cond.span.lo(), then.span.lo()), true) { self.print_if_cond("if", cond, then.span.lo()); - self.print_sep(Separator::Space); + // if empty block without comments, ensure braces are inlined + if let ast::StmtKind::Block(block) = &then.kind + && block.is_empty() + && self.peek_comment_before(then.span.hi()).is_none() + { + self.neverbreak(); + self.print_sep(Separator::Nbsp); + } else { + self.print_sep(Separator::Space); + } } self.end(); self.print_stmt_as_block(then, then.span.hi(), inline); @@ -1794,7 +1918,7 @@ impl<'ast> State<'_, 'ast> { self.nbsp(); }; self.print_path(path, false); - self.print_call_args(args); + self.print_call_args(args, false); } fn print_block(&mut self, block: &'ast [ast::Stmt<'ast>], span: Span) { @@ -1838,9 +1962,15 @@ impl<'ast> State<'_, 'ast> { self.neverbreak(); self.print_block_without_braces(stmts, pos_hi, None); } else { + // Reset cache for nested (child) stmts within this (parent) block. + let inline_parent = self.single_line_stmt.take(); + self.print_word("{"); self.print_block_without_braces(stmts, pos_hi, Some(self.ind)); self.print_word("}"); + + // Restore cache for the rest of stmts within the same height. + self.single_line_stmt = inline_parent; } } diff --git a/crates/fmt-2/src/state/yul.rs b/crates/fmt/src/state/yul.rs similarity index 93% rename from crates/fmt-2/src/state/yul.rs rename to crates/fmt/src/state/yul.rs index 3f988e1a12d1b..7275768cfac03 100644 --- a/crates/fmt-2/src/state/yul.rs +++ b/crates/fmt/src/state/yul.rs @@ -59,16 +59,16 @@ impl<'ast> State<'_, 'ast> { self.ibox(0); self.word("for "); - self.print_yul_block(init, span, self.can_yul_block_be_inlined(init), false); + self.print_yul_block(init, init.span, self.can_yul_block_be_inlined(init), false); self.space(); self.print_yul_expr(cond); self.space(); - self.print_yul_block(step, span, self.can_yul_block_be_inlined(step), false); + self.print_yul_block(step, step.span, self.can_yul_block_be_inlined(step), false); self.space(); - self.print_yul_block(body, span, self.can_yul_block_be_inlined(body), false); + self.print_yul_block(body, body.span, self.can_yul_block_be_inlined(body), false); self.end(); } @@ -104,6 +104,10 @@ impl<'ast> State<'_, 'ast> { yul::StmtKind::Continue => self.word("continue"), yul::StmtKind::FunctionDef(func) => { let yul::Function { name, parameters, returns, body } = func; + let params_hi = parameters + .last() + .map_or(returns.first().map_or(span.hi(), |r| r.span.lo()), |p| p.span.hi()); + self.cbox(0); self.s.ibox(0); self.word("function "); @@ -111,7 +115,7 @@ impl<'ast> State<'_, 'ast> { self.print_tuple( parameters, span.lo(), - span.hi(), + params_hi, Self::print_ident, get_span!(), ListFormat::Consistent { cmnts_break: false, with_space: false }, @@ -126,8 +130,8 @@ impl<'ast> State<'_, 'ast> { if has_returns { self.commasep( returns, - stmt.span.lo(), - stmt.span.hi(), + returns.first().map_or(params_hi, |ret| ret.span.lo()), + returns.last().map_or(span.hi(), |ret| ret.span.hi()), Self::print_ident, get_span!(), ListFormat::Yul { sym_prev: Some("->"), sym_post: Some("{") }, diff --git a/crates/fmt/src/string.rs b/crates/fmt/src/string.rs deleted file mode 100644 index 984c15423ed5a..0000000000000 --- a/crates/fmt/src/string.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Helpers for dealing with quoted strings - -/// The state of a character in a string with quotable components -/// This is a simplified version of the -/// [actual parser](https://docs.soliditylang.org/en/v0.8.15/grammar.html#a4.SolidityLexer.EscapeSequence) -/// as we don't care about hex or other character meanings -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum QuoteState { - /// Not currently in quoted string - #[default] - None, - /// The opening character of a quoted string - Opening(char), - /// A character in a quoted string - String(char), - /// The `\` in an escape sequence `"\n"` - Escaping(char), - /// The escaped character e.g. `n` in `"\n"` - Escaped(char), - /// The closing character - Closing(char), -} - -/// An iterator over characters and indices in a string slice with information about quoted string -/// states -pub struct QuoteStateCharIndices<'a> { - iter: std::str::CharIndices<'a>, - state: QuoteState, -} - -impl<'a> QuoteStateCharIndices<'a> { - fn new(string: &'a str) -> Self { - Self { iter: string.char_indices(), state: QuoteState::None } - } - pub fn with_state(mut self, state: QuoteState) -> Self { - self.state = state; - self - } -} - -impl Iterator for QuoteStateCharIndices<'_> { - type Item = (QuoteState, usize, char); - fn next(&mut self) -> Option { - let (idx, ch) = self.iter.next()?; - match self.state { - QuoteState::None | QuoteState::Closing(_) => { - if ch == '\'' || ch == '"' { - self.state = QuoteState::Opening(ch); - } else { - self.state = QuoteState::None - } - } - QuoteState::String(quote) | QuoteState::Opening(quote) | QuoteState::Escaped(quote) => { - if ch == quote { - self.state = QuoteState::Closing(quote) - } else if ch == '\\' { - self.state = QuoteState::Escaping(quote) - } else { - self.state = QuoteState::String(quote) - } - } - QuoteState::Escaping(quote) => self.state = QuoteState::Escaped(quote), - } - Some((self.state, idx, ch)) - } -} - -/// An iterator over the indices of quoted string locations -pub struct QuotedRanges<'a>(QuoteStateCharIndices<'a>); - -impl QuotedRanges<'_> { - pub fn with_state(mut self, state: QuoteState) -> Self { - self.0 = self.0.with_state(state); - self - } -} - -impl Iterator for QuotedRanges<'_> { - type Item = (char, usize, usize); - fn next(&mut self) -> Option { - let (quote, start) = loop { - let (state, idx, _) = self.0.next()?; - match state { - QuoteState::Opening(quote) - | QuoteState::Escaping(quote) - | QuoteState::Escaped(quote) - | QuoteState::String(quote) => break (quote, idx), - QuoteState::Closing(quote) => return Some((quote, idx, idx)), - QuoteState::None => {} - } - }; - for (state, idx, _) in self.0.by_ref() { - if matches!(state, QuoteState::Closing(_)) { - return Some((quote, start, idx)); - } - } - None - } -} - -/// Helpers for iterating over quoted strings -pub trait QuotedStringExt { - /// Returns an iterator of characters, indices and their quoted string state. - fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_>; - - /// Returns an iterator of quoted string ranges. - fn quoted_ranges(&self) -> QuotedRanges<'_> { - QuotedRanges(self.quote_state_char_indices()) - } - - /// Check to see if a string is quoted. This will return true if the first character - /// is a quote and the last character is a quote with no non-quoted sections in between. - fn is_quoted(&self) -> bool { - let mut iter = self.quote_state_char_indices(); - if !matches!(iter.next(), Some((QuoteState::Opening(_), _, _))) { - return false; - } - while let Some((state, _, _)) = iter.next() { - if matches!(state, QuoteState::Closing(_)) { - return iter.next().is_none(); - } - } - false - } -} - -impl QuotedStringExt for T -where - T: AsRef, -{ - fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> { - QuoteStateCharIndices::new(self.as_ref()) - } -} - -impl QuotedStringExt for str { - fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> { - QuoteStateCharIndices::new(self) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use similar_asserts::assert_eq; - - #[test] - fn quote_state_char_indices() { - assert_eq!( - r#"a'a"\'\"\n\\'a"#.quote_state_char_indices().collect::>(), - vec![ - (QuoteState::None, 0, 'a'), - (QuoteState::Opening('\''), 1, '\''), - (QuoteState::String('\''), 2, 'a'), - (QuoteState::String('\''), 3, '"'), - (QuoteState::Escaping('\''), 4, '\\'), - (QuoteState::Escaped('\''), 5, '\''), - (QuoteState::Escaping('\''), 6, '\\'), - (QuoteState::Escaped('\''), 7, '"'), - (QuoteState::Escaping('\''), 8, '\\'), - (QuoteState::Escaped('\''), 9, 'n'), - (QuoteState::Escaping('\''), 10, '\\'), - (QuoteState::Escaped('\''), 11, '\\'), - (QuoteState::Closing('\''), 12, '\''), - (QuoteState::None, 13, 'a'), - ] - ); - } - - #[test] - fn quoted_ranges() { - let string = r#"testing "double quoted" and 'single quoted' strings"#; - assert_eq!( - string - .quoted_ranges() - .map(|(quote, start, end)| (quote, &string[start..=end])) - .collect::>(), - vec![('"', r#""double quoted""#), ('\'', "'single quoted'")] - ); - } -} diff --git a/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol b/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol index 5a5f4fad50ff9..f7ff46fd7cd2c 100644 --- a/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol @@ -728,4 +728,13 @@ contract FunctionOverrides is { a = 1; } + + function simple( + address _target, + bytes memory _payload + ) + internal + { + a = 1; + } } diff --git a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol index 6c0d422ca998d..4afce0bf0f304 100644 --- a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol @@ -726,4 +726,10 @@ contract FunctionOverrides is { a = 1; } + + function simple(address _target, bytes memory _payload) + internal + { + a = 1; + } } diff --git a/crates/fmt/testdata/FunctionDefinition/fmt.sol b/crates/fmt/testdata/FunctionDefinition/fmt.sol index aebfd26c42650..3d8ecaed032cd 100644 --- a/crates/fmt/testdata/FunctionDefinition/fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/fmt.sol @@ -705,4 +705,10 @@ contract FunctionOverrides is { a = 1; } + + function simple(address _target, bytes memory _payload) + internal + { + a = 1; + } } diff --git a/crates/fmt/testdata/FunctionDefinition/original.sol b/crates/fmt/testdata/FunctionDefinition/original.sol index a416fc98de47b..02eb64e8e6fb0 100644 --- a/crates/fmt/testdata/FunctionDefinition/original.sol +++ b/crates/fmt/testdata/FunctionDefinition/original.sol @@ -214,4 +214,11 @@ contract FunctionOverrides is FunctionInterfaces, FunctionDefinitions { function oneParam(uint256 x) override(FunctionInterfaces, FunctionDefinitions, SomeOtherFunctionContract, SomeImport.AndAnotherFunctionContract) { a = 1; } + + function simple(address _target, bytes memory _payload) + internal + { + a = 1; + } + } diff --git a/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol b/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol index 4c2ea4953e6aa..5d62b4411da9e 100644 --- a/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol @@ -706,4 +706,10 @@ contract FunctionOverrides is { a = 1; } + + function simple(address _target, bytes memory _payload) + internal + { + a = 1; + } } diff --git a/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol b/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol index 88139d97d81b0..c91b84383f787 100644 --- a/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol @@ -712,4 +712,11 @@ contract FunctionOverrides is { a = 1; } + + function simple( + address _target, + bytes memory _payload + ) internal { + a = 1; + } } diff --git a/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol b/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol index 016d52a1bedd0..24052f8066b75 100644 --- a/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol @@ -706,4 +706,11 @@ contract FunctionOverrides is { a = 1; } + + function simple( + address _target, + bytes memory _payload + ) internal { + a = 1; + } } diff --git a/crates/fmt/testdata/IfStatement/block-multi.fmt.sol b/crates/fmt/testdata/IfStatement/block-multi.fmt.sol index a3e932c61fe27..e69a4fc613541 100644 --- a/crates/fmt/testdata/IfStatement/block-multi.fmt.sol +++ b/crates/fmt/testdata/IfStatement/block-multi.fmt.sol @@ -168,4 +168,33 @@ contract IfStatement { executeElse(); } } + + function test_nestedBkocks() public { + if (accesses[i].account == address(simpleStorage)) { + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + bytes32 slot = accesses[i].storageAccesses[j].slot; + if (slot == bytes32(uint256(0))) { + foundValueSlot = true; + } + if (slot == bytes32(uint256(1))) { + foundOwnerSlot = true; + } + if (slot == bytes32(uint256(2))) { + foundValuesSlot0 = true; + } + if (slot == bytes32(uint256(3))) { + foundValuesSlot1 = true; + } + if (slot == bytes32(uint256(4))) { + foundValuesSlot2 = true; + } + } + } + } + + function test_emptyIfBlock() external { + if (block.number < 10) {} else { + revert(); + } + } } diff --git a/crates/fmt/testdata/IfStatement/block-single.fmt.sol b/crates/fmt/testdata/IfStatement/block-single.fmt.sol index 8f5f299cb37f4..92a2067bd98f8 100644 --- a/crates/fmt/testdata/IfStatement/block-single.fmt.sol +++ b/crates/fmt/testdata/IfStatement/block-single.fmt.sol @@ -120,4 +120,23 @@ contract IfStatement { else if (condition) execute(); else executeElse(); } + + function test_nestedBkocks() public { + if (accesses[i].account == address(simpleStorage)) { + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + bytes32 slot = accesses[i].storageAccesses[j].slot; + if (slot == bytes32(uint256(0))) foundValueSlot = true; + if (slot == bytes32(uint256(1))) foundOwnerSlot = true; + if (slot == bytes32(uint256(2))) foundValuesSlot0 = true; + if (slot == bytes32(uint256(3))) foundValuesSlot1 = true; + if (slot == bytes32(uint256(4))) foundValuesSlot2 = true; + } + } + } + + function test_emptyIfBlock() external { + if (block.number < 10) {} else { + revert(); + } + } } diff --git a/crates/fmt/testdata/IfStatement/fmt.sol b/crates/fmt/testdata/IfStatement/fmt.sol index ebd685052867d..03117f0883773 100644 --- a/crates/fmt/testdata/IfStatement/fmt.sol +++ b/crates/fmt/testdata/IfStatement/fmt.sol @@ -142,4 +142,23 @@ contract IfStatement { executeElse(); } } + + function test_nestedBkocks() public { + if (accesses[i].account == address(simpleStorage)) { + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + bytes32 slot = accesses[i].storageAccesses[j].slot; + if (slot == bytes32(uint256(0))) foundValueSlot = true; + if (slot == bytes32(uint256(1))) foundOwnerSlot = true; + if (slot == bytes32(uint256(2))) foundValuesSlot0 = true; + if (slot == bytes32(uint256(3))) foundValuesSlot1 = true; + if (slot == bytes32(uint256(4))) foundValuesSlot2 = true; + } + } + } + + function test_emptyIfBlock() external { + if (block.number < 10) {} else { + revert(); + } + } } diff --git a/crates/fmt/testdata/IfStatement/original.sol b/crates/fmt/testdata/IfStatement/original.sol index b36829bbbf6bf..cef566fda3c8b 100644 --- a/crates/fmt/testdata/IfStatement/original.sol +++ b/crates/fmt/testdata/IfStatement/original.sol @@ -15,9 +15,9 @@ function executeWithVeryVeryVeryLongNameAndSomeParameter(bool parameter) {} contract IfStatement { function test() external { - if( true) + if( true) { - execute() ; + execute() ; } bool condition; bool anotherLongCondition; bool andAnotherVeryVeryLongCondition ; @@ -51,7 +51,7 @@ contract IfStatement { /* comment14 */ else { } // comment15 if ( - // comment16 + // comment16 condition /* comment17 */ ) { @@ -116,4 +116,23 @@ contract IfStatement { else executeElse(); } -} \ No newline at end of file + + function test_nestedBkocks() public { + if (accesses[i].account == address(simpleStorage)) { + for (uint256 j = 0; j < accesses[i].storageAccesses.length; j++) { + bytes32 slot = accesses[i].storageAccesses[j].slot; + if (slot == bytes32(uint256(0))) foundValueSlot = true; + if (slot == bytes32(uint256(1))) foundOwnerSlot = true; + if (slot == bytes32(uint256(2))) foundValuesSlot0 = true; + if (slot == bytes32(uint256(3))) foundValuesSlot1 = true; + if (slot == bytes32(uint256(4))) foundValuesSlot2 = true; + } + } + } + + function test_emptyIfBlock() external { + if (block.number < 10) {} else { + revert(); + } + } +} diff --git a/crates/fmt/testdata/LiteralExpression/fmt.sol b/crates/fmt/testdata/LiteralExpression/fmt.sol index 4c845185f4efb..1551b4a9cb316 100644 --- a/crates/fmt/testdata/LiteralExpression/fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/fmt.sol @@ -22,6 +22,8 @@ contract LiteralExpressions { 0x00; 0x123_456; 0x2eff_abde; + address(0xCAFE); + address(0xBEEF); // rational number literals 0.1; diff --git a/crates/fmt/testdata/LiteralExpression/original.sol b/crates/fmt/testdata/LiteralExpression/original.sol index 37bf210359f7f..916e927da1ff0 100644 --- a/crates/fmt/testdata/LiteralExpression/original.sol +++ b/crates/fmt/testdata/LiteralExpression/original.sol @@ -20,6 +20,8 @@ contract LiteralExpressions { 0x00; 0x123_456; 0x2eff_abde; + address(0xCAFE); + address(0xBEEF); // rational number literals .1; diff --git a/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol b/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol index 3aafcef65e978..7e9f6e0b2ec79 100644 --- a/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol @@ -23,6 +23,8 @@ contract LiteralExpressions { 0x00; 0x123_456; 0x2eff_abde; + address(0xCAFE); + address(0xBEEF); // rational number literals 0.1; diff --git a/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol b/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol index 83ff3f94a4435..67244e687fe03 100644 --- a/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol @@ -23,6 +23,8 @@ contract LiteralExpressions { 0x00; 0x123_456; 0x2eff_abde; + address(0xCAFE); + address(0xBEEF); // rational number literals 0.1; diff --git a/crates/fmt/testdata/Repros/fmt.sol b/crates/fmt/testdata/Repros/fmt.sol index e4bac736482bf..0613009ef94e1 100644 --- a/crates/fmt/testdata/Repros/fmt.sol +++ b/crates/fmt/testdata/Repros/fmt.sol @@ -176,3 +176,60 @@ function argListRepro(address tokenIn, uint256 amountIn, bool data) { data ); } + +contract NestedCallsTest is Test { + string constant errMsg = "User provided message"; + uint256 constant maxDecimals = 77; + + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_nestedCalls() public { + vm._expectCheatcodeRevert( + bytes(string.concat(errMsg, ": ", left, " != ", right)) + ); + } + + function test_assemblyFnComments() public { + assembly { + function setJPoint(i, x, y, z) { + // We will multiply by `0x80` (i.e. `shl(7, i)`) instead + // since the memory expansion costs are cheaper than doing `mul(0x60, i)`. + // Also help combine the lookup expression for `u1` and `u2` in `jMultShamir`. + i := shl(7, i) + mstore(i, x) + mstore(add(i, returndatasize()), y) + mstore(add(i, 0x40), z) + } + } + } + + function test_binOpsInsideNestedBlocks() public { + for (uint256 i = 0; i < steps.length; i++) { + if ( + step.opcode == 0x52 + && /*MSTORE*/ step.stack[0] == testContract.memPtr() // MSTORE offset + && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val + ) { + mstoreCalled = true; + } + } + } + + function test_longCall() public { + uint256 fork = + vm.createSelectFork("polygon", bytes32(0xdeadc0ffeedeadbeef)); + + vm._expectCheatcodeRevert("short msg doesn't break"); + vm._expectCheatcodeRevert( + "failed parsing as `uint256`: missing hex prefix for hex string" + ); + + bytes4[] memory targets = new bytes4[](0); + targets[0] = + FuzzArtifactSelector("TargetArtifactSelectors.t.sol:Hi", selectors); + + ConstructorVictim victim = new ConstructorVictim( + sender, "msg.sender", "not set during prank" + ); + } +} diff --git a/crates/fmt/testdata/Repros/original.sol b/crates/fmt/testdata/Repros/original.sol index f7594a663d5fb..e207f6aecea80 100644 --- a/crates/fmt/testdata/Repros/original.sol +++ b/crates/fmt/testdata/Repros/original.sol @@ -177,3 +177,58 @@ function argListRepro(address tokenIn, uint256 amountIn, bool data) { data ); } + +contract NestedCallsTest is Test { + string constant errMsg = "User provided message"; + uint256 constant maxDecimals = 77; + + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_nestedCalls() public { + vm._expectCheatcodeRevert( + bytes(string.concat(errMsg, ": ", left, " != ", right)) + ); + } + + function test_assemblyFnComments() public { + assembly { + function setJPoint(i, x, y, z) { + // We will multiply by `0x80` (i.e. `shl(7, i)`) instead + // since the memory expansion costs are cheaper than doing `mul(0x60, i)`. + // Also help combine the lookup expression for `u1` and `u2` in `jMultShamir`. + i := shl(7, i) + mstore(i, x) + mstore(add(i, returndatasize()), y) + mstore(add(i, 0x40), z) + } + } + } + + function test_binOpsInsideNestedBlocks() public { + for (uint256 i = 0; i < steps.length; i++) { + if ( + step.opcode == 0x52 + && /*MSTORE*/ step.stack[0] == testContract.memPtr() // MSTORE offset + && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val + ) { + mstoreCalled = true; + } + } + } + + function test_longCall() public { + uint256 fork = + vm.createSelectFork("polygon", bytes32(0xdeadc0ffeedeadbeef)); + + vm._expectCheatcodeRevert("short msg doesn't break"); + vm._expectCheatcodeRevert("failed parsing as `uint256`: missing hex prefix for hex string"); + + bytes4[] memory targets = new bytes4[](0); + targets[0] = + FuzzArtifactSelector("TargetArtifactSelectors.t.sol:Hi", selectors); + + ConstructorVictim victim = + new ConstructorVictim(sender, "msg.sender", "not set during prank"); + + } +} diff --git a/crates/fmt/testdata/Repros/sorted.fmt.sol b/crates/fmt/testdata/Repros/sorted.fmt.sol index 6758f6b39ef49..c14020994156f 100644 --- a/crates/fmt/testdata/Repros/sorted.fmt.sol +++ b/crates/fmt/testdata/Repros/sorted.fmt.sol @@ -177,3 +177,60 @@ function argListRepro(address tokenIn, uint256 amountIn, bool data) { data ); } + +contract NestedCallsTest is Test { + string constant errMsg = "User provided message"; + uint256 constant maxDecimals = 77; + + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_nestedCalls() public { + vm._expectCheatcodeRevert( + bytes(string.concat(errMsg, ": ", left, " != ", right)) + ); + } + + function test_assemblyFnComments() public { + assembly { + function setJPoint(i, x, y, z) { + // We will multiply by `0x80` (i.e. `shl(7, i)`) instead + // since the memory expansion costs are cheaper than doing `mul(0x60, i)`. + // Also help combine the lookup expression for `u1` and `u2` in `jMultShamir`. + i := shl(7, i) + mstore(i, x) + mstore(add(i, returndatasize()), y) + mstore(add(i, 0x40), z) + } + } + } + + function test_binOpsInsideNestedBlocks() public { + for (uint256 i = 0; i < steps.length; i++) { + if ( + step.opcode == 0x52 + && /*MSTORE*/ step.stack[0] == testContract.memPtr() // MSTORE offset + && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val + ) { + mstoreCalled = true; + } + } + } + + function test_longCall() public { + uint256 fork = + vm.createSelectFork("polygon", bytes32(0xdeadc0ffeedeadbeef)); + + vm._expectCheatcodeRevert("short msg doesn't break"); + vm._expectCheatcodeRevert( + "failed parsing as `uint256`: missing hex prefix for hex string" + ); + + bytes4[] memory targets = new bytes4[](0); + targets[0] = + FuzzArtifactSelector("TargetArtifactSelectors.t.sol:Hi", selectors); + + ConstructorVictim victim = new ConstructorVictim( + sender, "msg.sender", "not set during prank" + ); + } +} diff --git a/crates/fmt/testdata/Repros/tab.fmt.sol b/crates/fmt/testdata/Repros/tab.fmt.sol index e504869fd5729..66fd2b954ca8a 100644 --- a/crates/fmt/testdata/Repros/tab.fmt.sol +++ b/crates/fmt/testdata/Repros/tab.fmt.sol @@ -177,3 +177,60 @@ function argListRepro(address tokenIn, uint256 amountIn, bool data) { data ); } + +contract NestedCallsTest is Test { + string constant errMsg = "User provided message"; + uint256 constant maxDecimals = 77; + + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_nestedCalls() public { + vm._expectCheatcodeRevert( + bytes(string.concat(errMsg, ": ", left, " != ", right)) + ); + } + + function test_assemblyFnComments() public { + assembly { + function setJPoint(i, x, y, z) { + // We will multiply by `0x80` (i.e. `shl(7, i)`) instead + // since the memory expansion costs are cheaper than doing `mul(0x60, i)`. + // Also help combine the lookup expression for `u1` and `u2` in `jMultShamir`. + i := shl(7, i) + mstore(i, x) + mstore(add(i, returndatasize()), y) + mstore(add(i, 0x40), z) + } + } + } + + function test_binOpsInsideNestedBlocks() public { + for (uint256 i = 0; i < steps.length; i++) { + if ( + step.opcode == 0x52 + && /*MSTORE*/ step.stack[0] == testContract.memPtr() // MSTORE offset + && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val + ) { + mstoreCalled = true; + } + } + } + + function test_longCall() public { + uint256 fork = + vm.createSelectFork("polygon", bytes32(0xdeadc0ffeedeadbeef)); + + vm._expectCheatcodeRevert("short msg doesn't break"); + vm._expectCheatcodeRevert( + "failed parsing as `uint256`: missing hex prefix for hex string" + ); + + bytes4[] memory targets = new bytes4[](0); + targets[0] = + FuzzArtifactSelector("TargetArtifactSelectors.t.sol:Hi", selectors); + + ConstructorVictim victim = new ConstructorVictim( + sender, "msg.sender", "not set during prank" + ); + } +} diff --git a/crates/fmt/testdata/TryStatement/fmt.sol b/crates/fmt/testdata/TryStatement/fmt.sol index a7d46744f2d96..eab6cc5c18a5c 100644 --- a/crates/fmt/testdata/TryStatement/fmt.sol +++ b/crates/fmt/testdata/TryStatement/fmt.sol @@ -70,4 +70,14 @@ contract TryStatement { unknown.handleError(); } catch {} } + + function test_multiParam() { + Mock mock = new Mock(); + + try mock.add(2, 3) { + revert(); + } catch (bytes memory err) { + require(keccak256(err) == keccak256(ERROR_MESSAGE)); + } + } } diff --git a/crates/fmt/testdata/TryStatement/original.sol b/crates/fmt/testdata/TryStatement/original.sol index 8edd0117eee3f..ce5a2e82e1e7d 100644 --- a/crates/fmt/testdata/TryStatement/original.sol +++ b/crates/fmt/testdata/TryStatement/original.sol @@ -63,4 +63,14 @@ contract TryStatement { unknown.handleError(); } catch {} } + + function test_multiParam() { + Mock mock = new Mock(); + + try mock.add(2, 3) { + revert(); + } catch (bytes memory err) { + require(keccak256(err) == keccak256(ERROR_MESSAGE)); + } + } } diff --git a/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol b/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol index 0f871ea15baee..2bf3415bcce22 100644 --- a/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol @@ -8,9 +8,8 @@ contract TestContract { (, uint256 second) = (1, 2); (uint256 listItem001) = 1; (uint256 listItem002, uint256 listItem003) = (10, 20); - (uint256 listItem004, uint256 listItem005, uint256 listItem006) = ( - 10, 20, 30 - ); + (uint256 listItem004, uint256 listItem005, uint256 listItem006) = + (10, 20, 30); ( uint256 listItem007, uint256 listItem008, @@ -25,4 +24,18 @@ contract TestContract { uint256 allowed = allowance[from][msg.sender]; allowance[from][msg.sender] = allowed; } + + function test_longAssignements() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = + "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + } + + function test_stringConcatenation() public { + string memory strConcat = "0," "11579208923731619542357098500868790785," + "0x0000000000000000000000000000000000000000000000000000000000000000," + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + } } diff --git a/crates/fmt/testdata/VariableAssignment/fmt.sol b/crates/fmt/testdata/VariableAssignment/fmt.sol index 8cf9880f1d5ed..f77ca77545552 100644 --- a/crates/fmt/testdata/VariableAssignment/fmt.sol +++ b/crates/fmt/testdata/VariableAssignment/fmt.sol @@ -7,9 +7,8 @@ contract TestContract { (, uint256 second) = (1, 2); (uint256 listItem001) = 1; (uint256 listItem002, uint256 listItem003) = (10, 20); - (uint256 listItem004, uint256 listItem005, uint256 listItem006) = ( - 10, 20, 30 - ); + (uint256 listItem004, uint256 listItem005, uint256 listItem006) = + (10, 20, 30); ( uint256 listItem007, uint256 listItem008, @@ -24,4 +23,18 @@ contract TestContract { uint256 allowed = allowance[from][msg.sender]; allowance[from][msg.sender] = allowed; } + + function test_longAssignements() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = + "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + } + + function test_stringConcatenation() public { + string memory strConcat = "0," "11579208923731619542357098500868790785," + "0x0000000000000000000000000000000000000000000000000000000000000000," + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + } } diff --git a/crates/fmt/testdata/VariableAssignment/original.sol b/crates/fmt/testdata/VariableAssignment/original.sol index 07480d873c21e..8f79a5b06dc3f 100644 --- a/crates/fmt/testdata/VariableAssignment/original.sol +++ b/crates/fmt/testdata/VariableAssignment/original.sol @@ -23,4 +23,17 @@ contract TestContract { uint256 allowed = allowance[from][msg.sender]; allowance[from][msg.sender] = allowed; } + + function test_longAssignements() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[2] = "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + } + + function test_stringConcatenation() public { + string memory strConcat = "0," "11579208923731619542357098500868790785," + "0x0000000000000000000000000000000000000000000000000000000000000000," + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + } } diff --git a/crates/fmt/testdata/Yul/fmt.sol b/crates/fmt/testdata/Yul/fmt.sol index 6f280784ce640..000988e0d3e9c 100644 --- a/crates/fmt/testdata/Yul/fmt.sol +++ b/crates/fmt/testdata/Yul/fmt.sol @@ -129,7 +129,7 @@ contract Yul { sstore(gByte(caller(), 0x5), 0x1) sstore( 0x3212643709c27e33a5245e3719959b915fa892ed21a95cefee2f1fb126ea6810, - 0x726F105396F2CA1CCeBD5BFC27B556699A07FFE7C2 + 0x726F105396F2CA1CCEBD5BFC27B556699A07FFE7C2 ) } } diff --git a/crates/fmt/tests/formatter.rs b/crates/fmt/tests/formatter.rs index 447203e0fd690..cc4415090ce4d 100644 --- a/crates/fmt/tests/formatter.rs +++ b/crates/fmt/tests/formatter.rs @@ -1,245 +1,244 @@ -// use forge_fmt::{FormatterConfig, format_to, parse, solang_ext::AstEq}; -// use itertools::Itertools; -// use std::{fs, path::PathBuf}; -// use tracing_subscriber::{EnvFilter, FmtSubscriber}; - -// fn tracing() { -// let subscriber = FmtSubscriber::builder() -// .with_env_filter(EnvFilter::from_default_env()) -// .with_test_writer() -// .finish(); -// let _ = tracing::subscriber::set_global_default(subscriber); -// } - -// fn test_directory(base_name: &str, test_config: TestConfig) { -// tracing(); -// let mut original = None; - -// let tests = -// fs::read_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join(base_name)) -// .unwrap() -// .filter_map(|path| { -// let path = path.unwrap().path(); -// let source = fs::read_to_string(&path).unwrap(); - -// if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { -// if filename == "original.sol" { -// original = Some(source); -// } else if filename -// .strip_suffix("fmt.sol") -// .map(|filename| filename.strip_suffix('.')) -// .is_some() -// { -// // The majority of the tests were written with the assumption -// // that the default value for max line length is `80`. -// // Preserve that to avoid rewriting test logic. -// let default_config = -// FormatterConfig { line_length: 80, ..Default::default() }; - -// let mut config = toml::Value::try_from(default_config).unwrap(); -// let config_table = config.as_table_mut().unwrap(); -// let mut lines = source.split('\n').peekable(); -// let mut line_num = 1; -// while let Some(line) = lines.peek() { -// let entry = line -// .strip_prefix("//") -// .and_then(|line| line.trim().strip_prefix("config:")) -// .map(str::trim); -// let entry = if let Some(entry) = entry { entry } else { break }; - -// let values = match toml::from_str::(entry) { -// Ok(toml::Value::Table(table)) => table, -// _ => panic!("Invalid config item in {filename} at {line_num}"), -// }; -// config_table.extend(values); - -// line_num += 1; -// lines.next(); -// } -// let config = config -// .try_into() -// .unwrap_or_else(|err| panic!("Invalid config for {filename}: -// {err}")); - -// return Some((filename.to_string(), config, lines.join("\n"))); -// } -// } - -// None -// }) -// .collect::>(); - -// for (filename, config, formatted) in tests { -// test_formatter( -// &filename, -// config, -// original.as_ref().expect("original.sol not found"), -// &formatted, -// test_config, -// ); -// } -// } - -// fn assert_eof(content: &str) { -// assert!(content.ends_with('\n') && !content.ends_with("\n\n")); -// } - -// fn test_formatter( -// filename: &str, -// config: FormatterConfig, -// source: &str, -// expected_source: &str, -// test_config: TestConfig, -// ) { -// #[derive(Eq)] -// struct PrettyString(String); - -// impl PartialEq for PrettyString { -// fn eq(&self, other: &Self) -> bool { -// self.0.lines().eq(other.0.lines()) -// } -// } - -// impl std::fmt::Debug for PrettyString { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// f.write_str(&self.0) -// } -// } - -// assert_eof(expected_source); - -// let source_parsed = match parse(source) { -// Ok(p) => p, -// Err(e) => panic!("{e}"), -// }; -// let expected_parsed = match parse(expected_source) { -// Ok(p) => p, -// Err(e) => panic!("{e}"), -// }; - -// if !test_config.skip_compare_ast_eq && !source_parsed.pt.ast_eq(&expected_parsed.pt) { -// similar_asserts::assert_eq!( -// source_parsed.pt, -// expected_parsed.pt, -// "(formatted Parse Tree == expected Parse Tree) in {}", -// filename -// ); -// } - -// let expected = PrettyString(expected_source.to_string()); - -// let mut source_formatted = String::new(); -// format_to(&mut source_formatted, source_parsed, config.clone()).unwrap(); -// assert_eof(&source_formatted); - -// let source_formatted = PrettyString(source_formatted); - -// similar_asserts::assert_eq!( -// source_formatted, -// expected, -// "(formatted == expected) in {}", -// filename -// ); - -// let mut expected_formatted = String::new(); -// format_to(&mut expected_formatted, expected_parsed, config).unwrap(); -// assert_eof(&expected_formatted); - -// let expected_formatted = PrettyString(expected_formatted); - -// similar_asserts::assert_eq!( -// expected_formatted, -// expected, -// "(formatted == expected) in {}", -// filename -// ); -// } - -// #[derive(Clone, Copy, Default)] -// struct TestConfig { -// /// Whether to compare the formatted source code AST with the original AST -// skip_compare_ast_eq: bool, -// } - -// impl TestConfig { -// fn skip_compare_ast_eq() -> Self { -// Self { skip_compare_ast_eq: true } -// } -// } - -// macro_rules! test_dir { -// ($dir:ident $(,)?) => { -// test_dir!($dir, Default::default()); -// }; -// ($dir:ident, $config:expr $(,)?) => { -// #[expect(non_snake_case)] -// #[test] -// fn $dir() { -// test_directory(stringify!($dir), $config); -// } -// }; -// } - -// macro_rules! test_directories { -// ($($dir:ident),+ $(,)?) => {$( -// test_dir!($dir); -// )+}; -// } - -// test_directories! { -// ConstructorDefinition, -// ConstructorModifierStyle, -// ContractDefinition, -// DocComments, -// EnumDefinition, -// ErrorDefinition, -// EventDefinition, -// FunctionDefinition, -// FunctionDefinitionWithFunctionReturns, -// FunctionType, -// ImportDirective, -// ModifierDefinition, -// StatementBlock, -// StructDefinition, -// TypeDefinition, -// UsingDirective, -// VariableDefinition, -// OperatorExpressions, -// WhileStatement, -// DoWhileStatement, -// ForStatement, -// IfStatement, -// IfStatement2, -// VariableAssignment, -// FunctionCallArgsStatement, -// RevertStatement, -// RevertNamedArgsStatement, -// ReturnStatement, -// TryStatement, -// ConditionalOperatorExpression, -// NamedFunctionCallExpression, -// ArrayExpressions, -// UnitExpression, -// ThisExpression, -// SimpleComments, -// LiteralExpression, -// Yul, -// YulStrings, -// IntTypes, -// InlineDisable, -// NumberLiteralUnderscore, -// HexUnderscore, -// FunctionCall, -// TrailingComma, -// PragmaDirective, -// Annotation, -// MappingType, -// EmitStatement, -// Repros, -// BlockComments, -// BlockCommentsFunction, -// EnumVariants, -// } - -// test_dir!(SortedImports, TestConfig::skip_compare_ast_eq()); -// test_dir!(NonKeywords, TestConfig::skip_compare_ast_eq()); +use forge_fmt::FormatterConfig; +use snapbox::{Data, assert_data_eq}; +use solar::sema::Compiler; +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, +}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; + +#[track_caller] +fn format(source: &str, path: &Path, fmt_config: Arc) -> String { + let mut compiler = Compiler::new( + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(), + ); + + match forge_fmt::format_source(source, Some(path), fmt_config, &mut compiler).into_result() { + Ok(formatted) => formatted, + Err(e) => panic!("failed to format {path:?}: {e}"), + } +} + +#[track_caller] +fn assert_eof(content: &str) { + assert!(content.ends_with('\n'), "missing trailing newline"); + assert!(!content.ends_with("\n\n"), "extra trailing newline"); +} + +fn enable_tracing() { + let subscriber = FmtSubscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_test_writer() + .finish(); + let _ = tracing::subscriber::set_global_default(subscriber); +} + +fn tests_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata") +} + +fn test_directory(base_name: &str) { + enable_tracing(); + let dir = tests_dir().join(base_name); + let mut original = fs::read_to_string(dir.join("original.sol")).unwrap(); + if cfg!(windows) { + original = original.replace("\r\n", "\n"); + } + let mut handles = vec![]; + for res in dir.read_dir().unwrap() { + let entry = res.unwrap(); + let path = entry.path(); + + let filename = path.file_name().and_then(|name| name.to_str()).unwrap(); + if filename == "original.sol" { + continue; + } + assert!(path.is_file(), "expected file: {path:?}"); + assert!(filename.ends_with("fmt.sol"), "unknown file: {path:?}"); + + let mut expected = fs::read_to_string(&path).unwrap(); + if cfg!(windows) { + expected = expected + .replace("\r\n", "\n") + .replace(r"\'", r"/'") + .replace(r#"\""#, r#"/""#) + .replace("\\\n", "/\n"); + } + + // The majority of the tests were written with the assumption that the default value for max + // line length is `80`. Preserve that to avoid rewriting test logic. + let default_config = FormatterConfig { line_length: 80, ..Default::default() }; + + let mut config = toml::Value::try_from(default_config).unwrap(); + let config_table = config.as_table_mut().unwrap(); + let mut comments_end = 0; + for (i, line) in expected.lines().enumerate() { + let line_num = i + 1; + let Some(entry) = line + .strip_prefix("//") + .and_then(|line| line.trim().strip_prefix("config:")) + .map(str::trim) + else { + break; + }; + + let values = match toml::from_str::(entry) { + Ok(toml::Value::Table(table)) => table, + r => panic!("invalid fmt config item in {filename} at {line_num}: {r:?}"), + }; + config_table.extend(values); + + comments_end += line.len() + 1; + } + let config = Arc::new( + config + .try_into::() + .unwrap_or_else(|err| panic!("invalid test config for {filename}: {err}")), + ); + + let original = original.clone(); + let tname = format!("{base_name}/{filename}"); + let spawn = move || { + test_formatter(&path, config.clone(), &original, &expected, comments_end); + }; + handles.push(std::thread::Builder::new().name(tname).spawn(spawn).unwrap()); + } + let results = handles.into_iter().map(|h| h.join()).collect::>(); + for result in results { + result.unwrap(); + } +} + +fn test_formatter( + expected_path: &Path, + config: Arc, + source: &str, + expected_source: &str, + comments_end: usize, +) { + let path = &*expected_path.with_file_name("original.sol"); + let expected_data = || { + let data = Data::read_from(expected_path, None); + if cfg!(windows) { + let content = data + .to_string() + .replace("\r\n", "\n") + .replace(r"\'", r"/'") + .replace(r#"\""#, r#"/""#) + .replace("\\\n", "/\n"); + Data::text(content) + } else { + data.raw() + } + }; + + let mut source_formatted = format(source, path, config.clone()); + // Inject `expected`'s comments, if any, so we can use the expected file as a snapshot. + source_formatted.insert_str(0, &expected_source[..comments_end]); + assert_data_eq!(&source_formatted, expected_data()); + assert_eof(&source_formatted); + + let mut expected_content = std::fs::read_to_string(expected_path).unwrap(); + if cfg!(windows) { + expected_content = expected_content.replace("\r\n", "\n"); + } + let expected_formatted = format(&expected_content, expected_path, config); + assert_data_eq!(&expected_formatted, expected_data()); + assert_eof(expected_source); + assert_eof(&expected_formatted); +} + +fn test_all_dirs_are_declared(dirs: &[&str]) { + let mut undeclared = vec![]; + for actual_dir in tests_dir().read_dir().unwrap().filter_map(Result::ok) { + let path = actual_dir.path(); + assert!(path.is_dir(), "expected directory: {path:?}"); + let actual_dir_name = path.file_name().unwrap().to_str().unwrap(); + if !dirs.contains(&actual_dir_name) { + undeclared.push(actual_dir_name.to_string()); + } + } + if !undeclared.is_empty() { + panic!( + "the following test directories are not declared in the test suite macro call: {undeclared:#?}" + ); + } +} + +macro_rules! fmt_tests { + ($($(#[$attr:meta])* $dir:ident),+ $(,)?) => { + #[test] + fn all_dirs_are_declared() { + test_all_dirs_are_declared(&[$(stringify!($dir)),*]); + } + + $( + #[allow(non_snake_case)] + #[test] + $(#[$attr])* + fn $dir() { + test_directory(stringify!($dir)); + } + )+ + }; +} + +fmt_tests! { + #[ignore = "annotations are not valid Solidity"] + Annotation, + ArrayExpressions, + BlockComments, + BlockCommentsFunction, + ConditionalOperatorExpression, + ConstructorDefinition, + ConstructorModifierStyle, + ContractDefinition, + DocComments, + DoWhileStatement, + EmitStatement, + EnumDefinition, + EnumVariants, + ErrorDefinition, + EventDefinition, + ForStatement, + FunctionCall, + FunctionCallArgsStatement, + FunctionDefinition, + FunctionDefinitionWithFunctionReturns, + FunctionType, + HexUnderscore, + IfStatement, + IfStatement2, + ImportDirective, + InlineDisable, + IntTypes, + LiteralExpression, + MappingType, + ModifierDefinition, + NamedFunctionCallExpression, + NonKeywords, + NumberLiteralUnderscore, + OperatorExpressions, + PragmaDirective, + Repros, + ReturnStatement, + RevertNamedArgsStatement, + RevertStatement, + SimpleComments, + SortedImports, + StatementBlock, + StructDefinition, + ThisExpression, + #[ignore = "Solar errors when parsing inputs with trailing commas"] + TrailingComma, + TryStatement, + TypeDefinition, + UnitExpression, + UsingDirective, + VariableAssignment, + VariableDefinition, + WhileStatement, + Yul, + YulStrings, +} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index d7582ac321ed8..cdf4fff211359 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -76,7 +76,6 @@ regex = { workspace = true, default-features = false } semver.workspace = true serde_json.workspace = true similar = { version = "2", features = ["inline"] } -solang-parser.workspace = true solar.workspace = true strum = { workspace = true, features = ["derive"] } thiserror.workspace = true diff --git a/crates/forge/src/cmd/fmt.rs b/crates/forge/src/cmd/fmt.rs index e1c4532572b63..a80705fc57ff3 100644 --- a/crates/forge/src/cmd/fmt.rs +++ b/crates/forge/src/cmd/fmt.rs @@ -1,18 +1,19 @@ use super::watch::WatchArgs; use clap::{Parser, ValueHint}; use eyre::{Context, Result}; -use forge_fmt::{format_to, parse}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; use foundry_common::fs; use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS}; use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; use rayon::prelude::*; use similar::{ChangeTag, TextDiff}; +use solar::sema::Compiler; use std::{ fmt::{self, Write}, io, io::{Read, Write as _}, path::{Path, PathBuf}, + sync::Arc, }; use yansi::{Color, Paint, Style}; @@ -98,97 +99,116 @@ impl FmtArgs { } }; - let format = |source: String, path: Option<&Path>| -> Result<_> { - let name = match path { - Some(path) => path.strip_prefix(&config.root).unwrap_or(path).display().to_string(), - None => "stdin".to_string(), - }; + // Handle stdin on its own + if let Input::Stdin(original) = input { + let formatted = forge_fmt::format(&original, config.fmt) + .into_result() + .map_err(|_| eyre::eyre!("failed to format stdin"))?; - let parsed = parse(&source).wrap_err_with(|| { - format!("Failed to parse Solidity code for {name}. Leaving source unchanged.") - })?; - - if !parsed.invalid_inline_config_items.is_empty() { - for (loc, warning) in &parsed.invalid_inline_config_items { - let mut lines = source[..loc.start().min(source.len())].split('\n'); - let col = lines.next_back().unwrap().len() + 1; - let row = lines.count() + 1; - sh_warn!("[{}:{}:{}] {}", name, row, col, warning)?; - } - } - - let mut output = String::new(); - format_to(&mut output, parsed, config.fmt.clone()).unwrap(); - - solang_parser::parse(&output, 0).map_err(|diags| { - eyre::eyre!( - "Failed to construct valid Solidity code for {name}. Leaving source unchanged.\n\ - Debug info: {diags:?}\n\ - Formatted output:\n\n{output}" - ) - })?; - - let diff = TextDiff::from_lines(&source, &output); - let new_format = diff.ratio() < 1.0; - if self.check || path.is_none() { + let diff = TextDiff::from_lines(&original, &formatted); + if diff.ratio() < 1.0 { if self.raw { - sh_print!("{output}")?; - } - - // If new format then compute diff summary. - if new_format { - return Ok(Some(format_diff_summary(&name, &diff))); + sh_print!("{formatted}")?; + } else { + sh_print!("{}", format_diff_summary("stdin", &diff))?; } - } else if let Some(path) = path { - // If new format then write it on disk. - if new_format { - fs::write(path, output)?; + if self.check { + std::process::exit(1); } } - Ok(None) - }; + return Ok(()); + } - let diffs = match input { - Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), + // Unwrap and check input paths + let paths_to_fmt = match input { Input::Paths(paths) => { if paths.is_empty() { sh_warn!( "Nothing to format.\n\ - HINT: If you are working outside of the project, \ - try providing paths to your source files: `forge fmt `" + HINT: If you are working outside of the project, \ + try providing paths to your source files: `forge fmt `" )?; return Ok(()); } paths - .par_iter() - .map(|path| { - let source = fs::read_to_string(path)?; - format(source, Some(path)) - }) - .collect() } - }?; + Input::Stdin(_) => unreachable!(), + }; + + let mut compiler = Compiler::new( + solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(), + ); + + // Disable import resolution, load files, and parse them. + if compiler + .enter_mut(|c| -> solar::interface::Result<()> { + let mut pcx = c.parse(); + pcx.set_resolve_imports(false); + pcx.load_files(paths_to_fmt)?; + pcx.parse(); + Ok(()) + }) + .is_err() + { + eyre::bail!("unable to parse sources"); + } + + // Format and, if necessary, check the diffs. + let res = compiler.enter(|c| -> Result<()> { + let gcx = c.gcx(); + let fmt_config = Arc::new(config.fmt); + let diffs: Vec = gcx + .sources + .raw + .par_iter() + .filter_map(|source_unit| { + let path = source_unit.file.name.as_real()?; + let original = &source_unit.file.src; + let formatted = forge_fmt::format_ast(&gcx, source_unit, fmt_config.clone()); + + if original.as_str() == formatted { + return None; + } - let mut diffs = diffs.iter().flatten(); - if let Some(first) = diffs.next() { - // This branch is only reachable with stdin or --check + if self.check { + let name = + path.strip_prefix(&config.root).unwrap_or(path).display().to_string(); + let summary = format_diff_summary( + &name, + &TextDiff::from_lines(original.as_str(), &formatted), + ); + Some(Ok(summary)) + } else { + match fs::write(path, formatted) { + Ok(_) => { + let _ = sh_println!("Formatted {}", path.display()); + None + } + Err(e) => Some(Err(eyre::eyre!( + "Failed to write to {}: {}", + path.display(), + e + ))), + } + } + }) + .collect::>()?; - if !self.raw { + if !diffs.is_empty() { + // This block is only reached in --check mode when files need formatting. let mut stdout = io::stdout().lock(); - let first = std::iter::once(first); - for (i, diff) in first.chain(diffs).enumerate() { + for (i, diff) in diffs.iter().enumerate() { if i > 0 { let _ = stdout.write_all(b"\n"); } let _ = stdout.write_all(diff.as_bytes()); } - } - - if self.check { std::process::exit(1); } - } + Ok(()) + }); + res?; Ok(()) } diff --git a/crates/forge/tests/cli/fmt.rs b/crates/forge/tests/cli/fmt.rs new file mode 100644 index 0000000000000..1964e4fb82f30 --- /dev/null +++ b/crates/forge/tests/cli/fmt.rs @@ -0,0 +1,76 @@ +//! Integration tests for `forge fmt` command + +use foundry_test_utils::{forgetest, forgetest_init}; +use std::{fs, io::Write}; + +const UNFORMATTED: &str = r#"// SPDX-License-Identifier: MIT +pragma solidity =0.8.30 ; + +contract Test { + uint256 public value ; + function setValue ( uint256 _value ) public { + value = _value ; + } +}"#; +const FORMATTED: &str = r#"// SPDX-License-Identifier: MIT +pragma solidity =0.8.30; + +contract Test { + uint256 public value; + + function setValue(uint256 _value) public { + value = _value; + } +} +"#; + +// Test that fmt can format a simple contract file +forgetest_init!(fmt_file, |prj, cmd| { + prj.add_source("FmtTest.sol", UNFORMATTED); + cmd.arg("fmt").arg("src/FmtTest.sol"); + cmd.assert_success(); + + // Check that the file was formatted + let formatted = fs::read_to_string(prj.root().join("src/FmtTest.sol")).unwrap(); + assert!(formatted.contains(FORMATTED)); +}); + +// Test that fmt can format from stdin +forgetest!(fmt_stdin, |_prj, cmd| { + cmd.args(["fmt", "-", "--raw"]); + cmd.stdin(move |mut stdin| { + stdin.write_all(UNFORMATTED.as_bytes()).unwrap(); + }); + + // Check the output contains formatted code + cmd.assert_success().stdout_eq(FORMATTED); +}); + +forgetest_init!(fmt_check_mode, |prj, cmd| { + // Run fmt --check on a well-formatted file + prj.add_source("Test.sol", FORMATTED); + cmd.arg("fmt").arg("--check").arg("src/Test.sol"); + cmd.assert_success(); + + // Run fmt --check on a mal-formatted file + prj.add_source("Test2.sol", UNFORMATTED); + let mut cmd2 = prj.forge_command(); + cmd2.arg("fmt").arg("--check").arg("src/Test2.sol"); + cmd2.assert_failure(); +}); + +forgetest!(fmt_check_mode_stdin, |_prj, cmd| { + // Run fmt --check with well-formatted stdin input + cmd.arg("fmt").arg("-").arg("--check"); + cmd.stdin(move |mut stdin| { + stdin.write_all(FORMATTED.as_bytes()).unwrap(); + }); + cmd.assert_success(); + + // Run fmt --check with mal-formatted stdin input + cmd.arg("fmt").arg("-").arg("--check"); + cmd.stdin(move |mut stdin| { + stdin.write_all(UNFORMATTED.as_bytes()).unwrap(); + }); + cmd.assert_failure(); +}); diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index 4a59aa2b51e63..f51e96fff9387 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -31,4 +31,5 @@ mod verify_bytecode; mod version; mod ext_integration; +mod fmt; mod test_optimizer; diff --git a/env.sh b/env.sh index 78eaeea717217..35df5e39c58b0 100644 --- a/env.sh +++ b/env.sh @@ -1,7 +1,7 @@ -alias forge-fmt="cargo r --quiet -p forge-fmt-2 --" +alias forge-fmt="cargo r --quiet -p forge-fmt --" forge-fmt-cmp() { - cargo b --quiet -p forge-fmt-2 || return 1 - forge_fmt_new="$(pwd)/target/debug/forge-fmt-2" + cargo b --quiet -p forge-fmt || return 1 + forge_fmt_new="$(pwd)/target/debug/forge-fmt" tmp="$(mktemp -d)" in_f="$tmp/in.sol" diff --git a/testdata/default/cheats/AttachDelegation.t.sol b/testdata/default/cheats/AttachDelegation.t.sol index 3efc5f5229d19..05c7461928c87 100644 --- a/testdata/default/cheats/AttachDelegation.t.sol +++ b/testdata/default/cheats/AttachDelegation.t.sol @@ -204,8 +204,11 @@ contract AttachDelegationTest is DSTest { data: abi.encodeCall(ERC20.mint, (50, address(this))), value: 0 }); - calls[1] = - SimpleDelegateContract.Call({to: address(token), data: abi.encodeCall(ERC20.mint, (50, alice)), value: 0}); + calls[1] = SimpleDelegateContract.Call({ + to: address(token), + data: abi.encodeCall(ERC20.mint, (50, alice)), + value: 0 + }); vm.broadcast(bob_pk); SimpleDelegateContract(alice).execute(calls); diff --git a/testdata/default/cheats/ExpectEmit.t.sol b/testdata/default/cheats/ExpectEmit.t.sol index b6b09a8a8db70..ca91513443d21 100644 --- a/testdata/default/cheats/ExpectEmit.t.sol +++ b/testdata/default/cheats/ExpectEmit.t.sol @@ -408,7 +408,7 @@ contract ExpectEmitTest is DSTest { // vm.expectEmit(true, true, true, true); // emitter.doesNothing(); // emit Something(1, 2, 3, 4); - + // // // This should fail since `SomethingElse` in the test // // and in the `Emitter` contract have differing // // amounts of indexed topics. diff --git a/testdata/default/cheats/RecordAccountAccesses.t.sol b/testdata/default/cheats/RecordAccountAccesses.t.sol index 733daa6ac400e..88f795fafef99 100644 --- a/testdata/default/cheats/RecordAccountAccesses.t.sol +++ b/testdata/default/cheats/RecordAccountAccesses.t.sol @@ -1196,7 +1196,9 @@ contract RecordAccountAccessesTest is DSTest { deployedCode: "", initialized: true, value: 1 ether, - data: abi.encodePacked(type(SelfDestructor).creationCode, abi.encode(address(bytes20("doesn't exist yet")))), + data: abi.encodePacked( + type(SelfDestructor).creationCode, abi.encode(address(bytes20("doesn't exist yet"))) + ), reverted: false, storageAccesses: new Vm.StorageAccess[](0), depth: 3 diff --git a/testdata/default/cheats/RecordDebugTrace.t.sol b/testdata/default/cheats/RecordDebugTrace.t.sol index ade2e7aafb7e1..f66f701cc7ab4 100644 --- a/testdata/default/cheats/RecordDebugTrace.t.sol +++ b/testdata/default/cheats/RecordDebugTrace.t.sol @@ -88,14 +88,16 @@ contract RecordDebugTraceTest is DSTest { for (uint256 i = 0; i < steps.length; i++) { Vm.DebugStep memory step = steps[i]; if ( - step.opcode == 0x52 /*MSTORE*/ && step.stack[0] == testContract.memPtr() // MSTORE offset + step.opcode == 0x52 /*MSTORE*/ + && step.stack[0] == testContract.memPtr() // MSTORE offset && step.stack[1] == testContract.expectedValueInMemory() // MSTORE val ) { mstoreCalled = true; } if ( - step.opcode == 0x51 /*MLOAD*/ && step.stack[0] == testContract.memPtr() // MLOAD offset + step.opcode == 0x51 /*MLOAD*/ + && step.stack[0] == testContract.memPtr() // MLOAD offset && step.memoryInput.length == 32 // MLOAD should always load 32 bytes && uint256(bytes32(step.memoryInput)) == testContract.expectedValueInMemory() // MLOAD value ) { diff --git a/testdata/default/fork/DssExecLib.sol b/testdata/default/fork/DssExecLib.sol index 41becd090e658..5632d029d94fe 100644 --- a/testdata/default/fork/DssExecLib.sol +++ b/testdata/default/fork/DssExecLib.sol @@ -1356,8 +1356,8 @@ library DssExecLib { uint256 _end, uint256 _duration ) public returns (address) { - address lerp = - LerpFactoryLike(lerpFab()).newIlkLerp(_name, _target, _ilk, _what, _startTime, _start, _end, _duration); + address lerp = LerpFactoryLike(lerpFab()) + .newIlkLerp(_name, _target, _ilk, _what, _startTime, _start, _end, _duration); Authorizable(_target).rely(lerp); LerpLike(lerp).tick(); return lerp; diff --git a/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol b/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol index f8330a33cd6ec..817e65d179f81 100644 --- a/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol +++ b/testdata/default/fuzz/invariant/common/InvariantInnerContract.t.sol @@ -5,7 +5,7 @@ import "ds-test/test.sol"; /*////////////////////////////////////////////////////////////// Here we test that the fuzz engine can include a contract created during the fuzz - in its fuzz dictionary and eventually break the invariant. + in its fuzz dictionary and eventually break the invariant. Specifically, can Judas, a created contract from Jesus, break Jesus contract by revealing his identity. /*/ diff --git a/testdata/default/repros/Issue3703.t.sol b/testdata/default/repros/Issue3703.t.sol index 48651be24c669..7c21a46c46e4a 100644 --- a/testdata/default/repros/Issue3703.t.sol +++ b/testdata/default/repros/Issue3703.t.sol @@ -9,8 +9,9 @@ contract Issue3703Test is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); function setUp() public { - uint256 fork = - vm.createSelectFork("polygon", bytes32(0xbed0c8c1b9ff8bf0452979d170c52893bb8954f18a904aa5bcbd0f709be050b9)); + uint256 fork = vm.createSelectFork( + "polygon", bytes32(0xbed0c8c1b9ff8bf0452979d170c52893bb8954f18a904aa5bcbd0f709be050b9) + ); } function poolState(address poolAddr, uint256 expectedSqrtPriceX96, uint256 expectedLiquidity) private { diff --git a/testdata/default/repros/Issue8383.t.sol b/testdata/default/repros/Issue8383.t.sol index 339f5b518a30c..3c40e44475fcc 100644 --- a/testdata/default/repros/Issue8383.t.sol +++ b/testdata/default/repros/Issue8383.t.sol @@ -222,8 +222,11 @@ contract P256Verifier { yy := mulmod(y2, y2, p) zz := mulmod(z2, z2, p) s := mulmod(4, mulmod(x2, yy, p), p) - m := - addmod(mulmod(3, mulmod(x2, x2, p), p), mulmod(mload(returndatasize()), mulmod(zz, zz, p), p), p) + m := addmod( + mulmod(3, mulmod(x2, x2, p), p), + mulmod(mload(returndatasize()), mulmod(zz, zz, p), p), + p + ) x := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) z := mulmod(2, mulmod(y2, z2, p), p) y := addmod(mulmod(m, addmod(s, sub(p, x), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) @@ -266,8 +269,11 @@ contract P256Verifier { yy := mulmod(y2, y2, p) zz := mulmod(z2, z2, p) s := mulmod(4, mulmod(x2, yy, p), p) - m := - addmod(mulmod(3, mulmod(x2, x2, p), p), mulmod(mload(returndatasize()), mulmod(zz, zz, p), p), p) + m := addmod( + mulmod(3, mulmod(x2, x2, p), p), + mulmod(mload(returndatasize()), mulmod(zz, zz, p), p), + p + ) x := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) z := mulmod(2, mulmod(y2, z2, p), p) y := addmod(mulmod(m, addmod(s, sub(p, x), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)