Skip to content

Commit c127f25

Browse files
authored
Merge branch 'foundry-rs:master' into master
2 parents 63c5e9b + c6fcd87 commit c127f25

16 files changed

Lines changed: 1085 additions & 1 deletion

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/anvil/src/config.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ impl NodeConfig {
638638
pub fn set_chain_id(&mut self, chain_id: Option<impl Into<u64>>) {
639639
self.chain_id = chain_id.map(Into::into);
640640
let chain_id = self.get_chain_id();
641-
self.networks.with_chain_id(chain_id);
641+
self.networks = self.networks.with_chain_id(chain_id);
642642
self.genesis_accounts.iter_mut().for_each(|wallet| {
643643
*wallet = wallet.clone().with_chain_id(Some(chain_id));
644644
});
@@ -1774,4 +1774,13 @@ mod tests {
17741774
let config = PruneStateHistoryConfig::from_args(Some(Some(10)));
17751775
assert!(config.is_state_history_supported());
17761776
}
1777+
1778+
#[cfg(feature = "optimism")]
1779+
#[test]
1780+
fn set_chain_id_updates_network_config() {
1781+
let mut config = NodeConfig::test();
1782+
config.set_chain_id(Some(10u64));
1783+
1784+
assert!(config.networks.is_optimism());
1785+
}
17771786
}

crates/cli/src/json.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//! Shared JSON output primitives for Foundry CLIs.
2+
3+
use eyre::Result;
4+
use serde::{Deserialize, Serialize};
5+
use serde_json::{Value, to_string};
6+
7+
/// The current version of Foundry's top-level JSON output envelope.
8+
pub const JSON_SCHEMA_VERSION: u32 = 1;
9+
10+
/// Stable top-level envelope for complete machine-readable command output.
11+
///
12+
/// This envelope represents a terminal command outcome. Long-running commands
13+
/// that stream intermediate records should use a separate event type and reserve
14+
/// this shape for final, complete results.
15+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16+
pub struct JsonEnvelope<T> {
17+
/// Version of the envelope schema.
18+
pub schema_version: u32,
19+
/// Whether the command completed successfully.
20+
///
21+
/// Only meaningful for a complete/terminal command outcome.
22+
pub success: bool,
23+
/// Command-specific payload.
24+
pub data: Option<T>,
25+
/// Structured errors emitted by the command.
26+
pub errors: Vec<JsonMessage>,
27+
/// Structured warnings emitted by the command.
28+
pub warnings: Vec<JsonMessage>,
29+
}
30+
31+
impl<T> JsonEnvelope<T> {
32+
/// Creates a successful envelope with command-specific data.
33+
pub const fn success(data: T) -> Self {
34+
Self {
35+
schema_version: JSON_SCHEMA_VERSION,
36+
success: true,
37+
data: Some(data),
38+
errors: Vec::new(),
39+
warnings: Vec::new(),
40+
}
41+
}
42+
43+
/// Creates a successful envelope with command-specific data and warnings.
44+
pub const fn success_with_warnings(data: T, warnings: Vec<JsonMessage>) -> Self {
45+
Self {
46+
schema_version: JSON_SCHEMA_VERSION,
47+
success: true,
48+
data: Some(data),
49+
errors: Vec::new(),
50+
warnings,
51+
}
52+
}
53+
}
54+
55+
impl JsonEnvelope<()> {
56+
/// Creates a failed envelope with one structured error.
57+
pub fn error(error: JsonMessage) -> Self {
58+
Self::failure(vec![error])
59+
}
60+
61+
/// Creates a failed envelope with structured errors.
62+
pub const fn failure(errors: Vec<JsonMessage>) -> Self {
63+
Self {
64+
schema_version: JSON_SCHEMA_VERSION,
65+
success: false,
66+
data: None,
67+
errors,
68+
warnings: Vec::new(),
69+
}
70+
}
71+
}
72+
73+
/// Severity level for a structured JSON diagnostic.
74+
///
75+
/// These levels classify diagnostics attached to an envelope. Progress,
76+
/// informational, and debug records should be modeled as command output data or
77+
/// stream events instead.
78+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79+
#[serde(rename_all = "snake_case")]
80+
pub enum JsonMessageLevel {
81+
/// Error message.
82+
Error,
83+
/// Warning message.
84+
Warning,
85+
}
86+
87+
/// Structured diagnostic entry for JSON output.
88+
///
89+
/// Diagnostics describe errors and warnings associated with command output. They
90+
/// are not intended for progress, informational, or debug events.
91+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92+
pub struct JsonMessage {
93+
/// Diagnostic severity level.
94+
pub level: JsonMessageLevel,
95+
/// Stable machine-readable diagnostic code.
96+
pub code: String,
97+
/// Human-readable diagnostic message.
98+
pub message: String,
99+
/// Optional structured context for the diagnostic.
100+
#[serde(skip_serializing_if = "Option::is_none")]
101+
pub details: Option<Value>,
102+
}
103+
104+
impl JsonMessage {
105+
/// Creates a structured error without details.
106+
pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
107+
Self {
108+
level: JsonMessageLevel::Error,
109+
code: code.into(),
110+
message: message.into(),
111+
details: None,
112+
}
113+
}
114+
115+
/// Creates a structured warning without details.
116+
pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
117+
Self {
118+
level: JsonMessageLevel::Warning,
119+
code: code.into(),
120+
message: message.into(),
121+
details: None,
122+
}
123+
}
124+
125+
/// Adds structured details to the diagnostic.
126+
pub fn with_details(mut self, details: Value) -> Self {
127+
self.details = Some(details);
128+
self
129+
}
130+
}
131+
132+
/// Prints a value as compact, single-line JSON to stdout.
133+
///
134+
/// The trailing newline makes this suitable for NDJSON streams when each call
135+
/// emits one self-contained JSON record.
136+
pub fn print_json<T: Serialize>(value: &T) -> Result<()> {
137+
sh_println!("{}", to_string(value)?)?;
138+
Ok(())
139+
}
140+
141+
/// Prints a successful JSON envelope to stdout.
142+
pub fn print_json_success<T: Serialize>(data: T) -> Result<()> {
143+
print_json(&JsonEnvelope::success(data))
144+
}
145+
146+
/// Prints a successful JSON envelope with warnings to stdout.
147+
pub fn print_json_success_with_warnings<T: Serialize>(
148+
data: T,
149+
warnings: Vec<JsonMessage>,
150+
) -> Result<()> {
151+
print_json(&JsonEnvelope::success_with_warnings(data, warnings))
152+
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
use serde_json::{json, to_value};
158+
159+
#[derive(Serialize)]
160+
struct BuildData {
161+
contracts: usize,
162+
}
163+
164+
#[test]
165+
fn success_envelope_serializes_all_top_level_fields() {
166+
let envelope = JsonEnvelope::success(BuildData { contracts: 2 });
167+
168+
let json = to_string(&envelope).unwrap();
169+
170+
assert_eq!(
171+
json,
172+
r#"{"schema_version":1,"success":true,"data":{"contracts":2},"errors":[],"warnings":[]}"#
173+
);
174+
}
175+
176+
#[test]
177+
fn warning_details_are_structured() {
178+
let warning = JsonMessage::warning("compiler.remappings", "auto-detected remappings")
179+
.with_details(json!({ "count": 3 }));
180+
let envelope =
181+
JsonEnvelope::success_with_warnings(BuildData { contracts: 1 }, vec![warning]);
182+
183+
let value = to_value(&envelope).unwrap();
184+
185+
assert_eq!(value["success"], true);
186+
assert_eq!(value["warnings"][0]["level"], "warning");
187+
assert_eq!(value["warnings"][0]["code"], "compiler.remappings");
188+
assert_eq!(value["warnings"][0]["details"]["count"], 3);
189+
}
190+
191+
#[test]
192+
fn failure_envelope_serializes_null_data_and_structured_errors() {
193+
let error = JsonMessage::error("config.invalid", "invalid foundry.toml")
194+
.with_details(json!({ "path": "foundry.toml" }));
195+
let envelope = JsonEnvelope::error(error);
196+
197+
let value = to_value(&envelope).unwrap();
198+
199+
assert_eq!(value["schema_version"], JSON_SCHEMA_VERSION);
200+
assert_eq!(value["success"], false);
201+
assert!(value["data"].is_null());
202+
assert_eq!(value["errors"][0]["level"], "error");
203+
assert_eq!(value["errors"][0]["code"], "config.invalid");
204+
assert_eq!(value["errors"][0]["details"]["path"], "foundry.toml");
205+
assert_eq!(value["warnings"], json!([]));
206+
}
207+
}

crates/cli/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extern crate tracing;
1313

1414
pub mod clap;
1515
pub mod handler;
16+
pub mod json;
1617
pub mod opts;
1718
pub mod utils;
1819

crates/lint/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ foundry-common.workspace = true
1818
foundry-compilers.workspace = true
1919
foundry-config.workspace = true
2020

21+
alloy-primitives.workspace = true
2122
solar.workspace = true
2223

2324
eyre.workspace = true

crates/lint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ It helps enforce best practices and improve code quality within Foundry projects
4444
- `low-level-calls`: Direct use of low-level calls should be avoided.
4545
- **Gas Optimizations:**
4646
- `asm-keccak256`: Recommends using inline assembly for `keccak256` for potential gas savings.
47+
- `costly-loop`: Flags storage variable writes inside loops; accumulate into a local variable and write once after the loop instead.
4748
- `could-be-immutable`: Recommends declaring constructor-only state variables as `immutable`.
4849
- `custom-errors`: Recommends using custom errors instead of strings and plain reverts for potential gas savings.
4950
- `unused-state-variables`: State variables that are never used should be removed.

crates/lint/docs/costly-loop.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Costly operations inside a loop
2+
3+
**Severity**: `Gas`
4+
**ID**: `costly-loop`
5+
6+
Flags storage variable writes inside loops. Each SSTORE costs at least 2,900 gas (warm) or 20,000
7+
gas (cold), so writing to storage on every loop iteration can be extremely expensive. Accumulating
8+
the result in a local memory variable and writing to storage once after the loop is the standard
9+
optimization.
10+
11+
## What it does
12+
13+
Reports assignments, compound assignments, increments/decrements, and `delete` expressions that
14+
directly write to a storage variable inside any `for`, `while`, or `do-while` loop body, including
15+
writes through storage array indices and mapping keys.
16+
17+
## Why is this bad?
18+
19+
SSTORE is one of the most expensive EVM opcodes. Writing to storage in a loop multiplies that cost
20+
by the number of iterations and can easily cause transactions to run out of gas or become
21+
economically impractical.
22+
23+
## Example
24+
25+
### Bad
26+
27+
```solidity
28+
contract C {
29+
uint256 public counter;
30+
31+
function bad(uint256 n) external {
32+
for (uint256 i = 0; i < n; i++) {
33+
counter++; // costly-loop: SSTORE on every iteration
34+
}
35+
}
36+
}
37+
```
38+
39+
### Good
40+
41+
```solidity
42+
contract C {
43+
uint256 public counter;
44+
45+
function good(uint256 n) external {
46+
uint256 local = counter;
47+
for (uint256 i = 0; i < n; i++) {
48+
local++;
49+
}
50+
counter = local; // single SSTORE after the loop
51+
}
52+
}
53+
```
54+
55+
## Notes
56+
57+
This is a `Gas`-severity lint and is **not** applied to test or script files.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Type-Based Tautology
2+
3+
**Severity**: `Med`
4+
**ID**: `type-based-tautology`
5+
6+
Detects comparison expressions that are always true or always false due to the numeric range of the variable's type. These dead conditions indicate logic errors or misunderstandings about integer bounds.
7+
8+
## What it does
9+
10+
Flags binary comparisons (`<`, `<=`, `>`, `>=`, `==`, `!=`) where one operand is a typed integer variable and the other is a constant that lies outside, or exactly at the boundary of the variable's representable range, making the condition unconditionally true or false.
11+
12+
Examples:
13+
- `uint x >= 0` is always true because unsigned integers cannot be negative.
14+
- `uint8 x > 255` is always false because 255 is the maximum value of `uint8`.
15+
- `int8 x < -128` is always false because -128 is the minimum value of `int8`.
16+
- `uint8 x == 256` is always false because 256 is outside the range of `uint8`.
17+
- `uint8 x != 256` is always true because 256 is outside the range of `uint8`.
18+
19+
The check also applies to explicit type casts: `uint8(x) < 256` is always true.
20+
21+
> **Limitation:** The lint only fires when the left-hand variable is a local or state variable identifier, or an explicit cast expression (e.g. `uint8(x)`). It does not fire on struct member access (`s.field < 0`) or function return values (`foo() < 0`).
22+
23+
## Why is this bad?
24+
25+
A condition that is permanently true contributes no useful logic and may hide a bug where the developer intended to compare against a different value or use a differently sized type. A condition that is permanently false creates unreachable code, which can silently suppress intended behavior such as access control checks or error handling.
26+
27+
## Example
28+
29+
### Bad
30+
31+
```solidity
32+
function isValid(uint256 x) public pure returns (bool) {
33+
return x >= 0; // always true, uint cannot be negative
34+
}
35+
36+
function isInRange(uint8 x) public pure returns (bool) {
37+
return x < 256; // always true, uint8 max is 255
38+
}
39+
40+
function isBelowMin(int8 x) public pure returns (bool) {
41+
return x < -128; // always false, int8 min is -128
42+
}
43+
44+
function isImpossible(uint8 x) public pure returns (bool) {
45+
return x == 256; // always false, 256 is outside uint8 range
46+
}
47+
```
48+
49+
### Good
50+
51+
```solidity
52+
function isValid(uint256 x) public pure returns (bool) {
53+
return x > 0; // meaningful: false when x == 0
54+
}
55+
56+
function isInRange(uint8 x, uint8 limit) public pure returns (bool) {
57+
return x < limit; // compare against a runtime value
58+
}
59+
60+
function isBelowThreshold(int8 x) public pure returns (bool) {
61+
return x < -100; // a value within the representable range
62+
}
63+
```

0 commit comments

Comments
 (0)