• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20074615249

09 Dec 2025 06:40PM UTC coverage: 58.494% (+0.2%) from 58.279%
20074615249

push

github

fasterthanlime
feat(facet-core): add #[facet(metadata = kind)] field attribute

Adds a new `metadata` field attribute for marking fields that contain
auxiliary information (like source spans) that should be excluded from
structural equality and hashing.

Changes:
- Add `metadata: Option<&'static str>` field to `Field` struct
- Add `is_metadata()` and `metadata_kind()` helper methods
- Add `.metadata(kind)` builder method to `FieldBuilder`
- Handle `#[facet(metadata = kind)]` in derive macro
- Update `structural_hash` to skip metadata fields
- Update `Spanned<T>` to use `#[facet(metadata = span)]` on span field
- Update `is_spanned_shape()` to detect via metadata field
- Add `find_span_metadata_field()` helper function
- Update facet-json deserializer to use new metadata detection

This allows any struct to have span tracking by adding the metadata
attribute, not just the built-in `Spanned<T>` wrapper. Deserializers
can check `field.metadata_kind() == Some("span")` to know which field
to populate with source location information.

Also includes preparatory work for GumTree-style tree diffing:
- Add tree.rs module with tree building and diff algorithm
- Update showcase to demonstrate new tree diff output

359 of 495 new or added lines in 7 files covered. (72.53%)

1 existing line in 1 file now uncovered.

26860 of 45919 relevant lines covered (58.49%)

619.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.29
/facet-diff/src/display.rs
1
use std::fmt::{Display, Write};
2

3
use facet_pretty::{PrettyPrinter, tokyo_night};
4
use owo_colors::OwoColorize;
5

6
use crate::{
7
    confusables::{are_visually_confusable, format_confusable_diff},
8
    diff::{Diff, Value},
9
    sequences::{ReplaceGroup, Updates, UpdatesGroup},
10
};
11

12
/// Format text for deletions
13
fn deleted(s: &str) -> String {
121✔
14
    format!("{}", s.color(tokyo_night::DELETION))
121✔
15
}
121✔
16

17
/// Format text for insertions
18
fn inserted(s: &str) -> String {
124✔
19
    format!("{}", s.color(tokyo_night::INSERTION))
124✔
20
}
124✔
21

22
/// Format muted text (unchanged indicators, structural equality)
23
fn muted(s: &str) -> String {
112✔
24
    format!("{}", s.color(tokyo_night::MUTED))
112✔
25
}
112✔
26

27
/// Format field name
28
fn field(s: &str) -> String {
73✔
29
    format!("{}", s.color(tokyo_night::FIELD_NAME))
73✔
30
}
73✔
31

32
/// Format punctuation as dimmed
33
fn punct(s: &str) -> String {
235✔
34
    format!("{}", s.color(tokyo_night::COMMENT))
235✔
35
}
235✔
36

37
struct PadAdapter<'a, 'b: 'a> {
38
    fmt: &'a mut std::fmt::Formatter<'b>,
39
    on_newline: bool,
40
    indent: &'static str,
41
}
42

43
impl<'a, 'b> PadAdapter<'a, 'b> {
44
    fn new_indented(fmt: &'a mut std::fmt::Formatter<'b>) -> Self {
85✔
45
        Self {
85✔
46
            fmt,
85✔
47
            on_newline: true,
85✔
48
            indent: "  ",
85✔
49
        }
85✔
50
    }
85✔
51
}
52

53
impl<'a, 'b> Write for PadAdapter<'a, 'b> {
54
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
1,442✔
55
        for line in s.split_inclusive('\n') {
1,442✔
56
            if self.on_newline {
1,442✔
57
                self.fmt.write_str(self.indent)?;
334✔
58
            }
1,108✔
59

60
            self.on_newline = line.ends_with('\n');
1,442✔
61

62
            self.fmt.write_str(line)?;
1,442✔
63
        }
64

65
        Ok(())
1,442✔
66
    }
1,442✔
67

68
    fn write_char(&mut self, c: char) -> std::fmt::Result {
×
69
        if self.on_newline {
×
70
            self.fmt.write_str(self.indent)?;
×
71
        }
×
72

73
        self.on_newline = c == '\n';
×
74
        self.fmt.write_char(c)
×
75
    }
×
76
}
77

78
impl<'mem, 'facet> Display for Diff<'mem, 'facet> {
79
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239✔
80
        match self {
239✔
81
            Diff::Equal { value: _ } => {
82
                write!(f, "{}", muted("(structurally equal)"))
25✔
83
            }
84
            Diff::Replace { from, to } => {
103✔
85
                let printer = PrettyPrinter::default()
103✔
86
                    .with_colors(false)
103✔
87
                    .with_minimal_option_names(true);
103✔
88

89
                // Check if both values are strings and visually confusable
90
                if let (Some(from_str), Some(to_str)) = (from.as_str(), to.as_str())
103✔
91
                    && are_visually_confusable(from_str, to_str)
14✔
92
                {
93
                    // Show the strings with confusable explanation
NEW
94
                    write!(
×
NEW
95
                        f,
×
96
                        "{} → {}\n{}",
NEW
97
                        deleted(&printer.format_peek(*from)),
×
NEW
98
                        inserted(&printer.format_peek(*to)),
×
NEW
99
                        muted(&format_confusable_diff(from_str, to_str).unwrap_or_default())
×
NEW
100
                    )?;
×
NEW
101
                    return Ok(());
×
102
                }
103✔
103

104
                // Show value change inline: old → new
105
                write!(
103✔
106
                    f,
103✔
107
                    "{} → {}",
108
                    deleted(&printer.format_peek(*from)),
103✔
109
                    inserted(&printer.format_peek(*to))
103✔
110
                )
111
            }
112
            Diff::User {
113
                from: _,
114
                to: _,
115
                variant,
77✔
116
                value,
77✔
117
            } => {
118
                let printer = PrettyPrinter::default()
77✔
119
                    .with_colors(false)
77✔
120
                    .with_minimal_option_names(true);
77✔
121

122
                // Show variant if present (e.g., "Some" for Option::Some)
123
                if let Some(variant) = variant {
77✔
124
                    write!(f, "{}", variant.bold())?;
12✔
125
                }
65✔
126

127
                let has_prefix = variant.is_some();
77✔
128

129
                match value {
77✔
130
                    Value::Struct {
131
                        updates,
61✔
132
                        deletions,
61✔
133
                        insertions,
61✔
134
                        unchanged,
61✔
135
                    } => {
136
                        if updates.is_empty() && deletions.is_empty() && insertions.is_empty() {
61✔
137
                            return write!(f, "{}", muted("(structurally equal)"));
9✔
138
                        }
52✔
139

140
                        if has_prefix {
52✔
141
                            writeln!(f, " {}", punct("{"))?;
1✔
142
                        } else {
143
                            writeln!(f, "{}", punct("{"))?;
51✔
144
                        }
145
                        let mut indent = PadAdapter::new_indented(f);
52✔
146

147
                        // Show unchanged fields indicator first
148
                        let unchanged_count = unchanged.len();
52✔
149
                        if unchanged_count > 0 {
52✔
150
                            let label = if unchanged_count == 1 {
30✔
151
                                "field"
21✔
152
                            } else {
153
                                "fields"
9✔
154
                            };
155
                            writeln!(
30✔
156
                                indent,
30✔
157
                                "{}",
158
                                muted(&format!(".. {unchanged_count} unchanged {label}"))
30✔
159
                            )?;
×
160
                        }
22✔
161

162
                        // Sort fields for deterministic output
163
                        let mut updates: Vec<_> = updates.iter().collect();
52✔
164
                        updates.sort_by(|(a, _), (b, _)| a.cmp(b));
52✔
165
                        for (fld, update) in updates {
69✔
166
                            writeln!(indent, "{}{} {update}", field(fld), punct(":"))?;
69✔
167
                        }
168

169
                        let mut deletions: Vec<_> = deletions.iter().collect();
52✔
170
                        deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
52✔
171
                        for (fld, value) in deletions {
52✔
172
                            writeln!(
2✔
173
                                indent,
2✔
174
                                "{} {}{} {}",
175
                                deleted("-"),
2✔
176
                                field(fld),
2✔
177
                                punct(":"),
2✔
178
                                deleted(&printer.format_peek(*value))
2✔
179
                            )?;
×
180
                        }
181

182
                        let mut insertions: Vec<_> = insertions.iter().collect();
52✔
183
                        insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
52✔
184
                        for (fld, value) in insertions {
52✔
185
                            writeln!(
2✔
186
                                indent,
2✔
187
                                "{} {}{} {}",
188
                                inserted("+"),
2✔
189
                                field(fld),
2✔
190
                                punct(":"),
2✔
191
                                inserted(&printer.format_peek(*value))
2✔
192
                            )?;
×
193
                        }
194

195
                        write!(f, "{}", punct("}"))
52✔
196
                    }
197
                    Value::Tuple { updates } => {
16✔
198
                        // No changes in tuple
199
                        if updates.is_empty() {
16✔
200
                            return write!(f, "{}", muted("(structurally equal)"));
2✔
201
                        }
14✔
202
                        // For single-element tuples (like Option::Some), try to be concise
203
                        if updates.is_single_replace() {
14✔
204
                            if has_prefix {
10✔
205
                                f.write_str(" ")?;
7✔
206
                            }
3✔
207
                            write!(f, "{updates}")
10✔
208
                        } else {
209
                            f.write_str(if has_prefix { " (\n" } else { "(\n" })?;
4✔
210
                            let mut indent = PadAdapter::new_indented(f);
4✔
211
                            write!(indent, "{updates}")?;
4✔
212
                            f.write_str(")")
4✔
213
                        }
214
                    }
215
                }
216
            }
217
            Diff::Sequence {
218
                from: _,
219
                to: _,
220
                updates,
34✔
221
            } => {
222
                if updates.is_empty() {
34✔
223
                    write!(f, "{}", muted("(structurally equal)"))
5✔
224
                } else {
225
                    writeln!(f, "{}", punct("["))?;
29✔
226
                    let mut indent = PadAdapter::new_indented(f);
29✔
227
                    write!(indent, "{updates}")?;
29✔
228
                    write!(f, "{}", punct("]"))
29✔
229
                }
230
            }
231
        }
232
    }
239✔
233
}
234

235
impl<'mem, 'facet> Updates<'mem, 'facet> {
236
    /// Check if this is a single replace operation (useful for Option::Some)
237
    fn is_single_replace(&self) -> bool {
14✔
238
        self.0.first.is_some() && self.0.values.is_empty() && self.0.last.is_none()
14✔
239
    }
14✔
240

241
    /// Check if there are no changes (everything is unchanged)
242
    fn is_empty(&self) -> bool {
50✔
243
        self.0.first.is_none() && self.0.values.is_empty()
50✔
244
    }
50✔
245
}
246

247
impl<'mem, 'facet> Display for Updates<'mem, 'facet> {
248
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43✔
249
        if let Some(update) = &self.0.first {
43✔
250
            update.fmt(f)?;
26✔
251
        }
17✔
252

253
        for (values, update) in &self.0.values {
43✔
254
            // Collapse kept values into ".. N unchanged items"
255
            let count = values.len();
23✔
256
            if count > 0 {
23✔
257
                let label = if count == 1 { "item" } else { "items" };
23✔
258
                writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
23✔
259
            }
×
260
            update.fmt(f)?;
23✔
261
        }
262

263
        if let Some(values) = &self.0.last {
43✔
264
            // Collapse trailing kept values
265
            let count = values.len();
7✔
266
            if count > 0 {
7✔
267
                let label = if count == 1 { "item" } else { "items" };
7✔
268
                writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
7✔
269
            }
×
270
        }
36✔
271

272
        Ok(())
43✔
273
    }
43✔
274
}
275

276
impl<'mem, 'facet> Display for ReplaceGroup<'mem, 'facet> {
277
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32✔
278
        let printer = PrettyPrinter::default()
32✔
279
            .with_colors(false)
32✔
280
            .with_minimal_option_names(true);
32✔
281

282
        // If it's a 1-to-1 replacement, check for nested diff or equality
283
        if self.removals.len() == 1 && self.additions.len() == 1 {
32✔
284
            let from = self.removals[0];
14✔
285
            let to = self.additions[0];
14✔
286
            let diff = Diff::new_peek(from, to);
14✔
287

288
            match &diff {
14✔
289
                Diff::Equal { .. } => {
290
                    // Values are equal, show muted
291
                    return writeln!(f, "{}", muted(&printer.format_peek(from)));
×
292
                }
293
                Diff::Replace { .. } => {
294
                    // Simple value change, show inline: old → new
295
                    return writeln!(f, "{diff}");
14✔
296
                }
297
                _ => {
298
                    // Has nested structure, show the diff
299
                    return writeln!(f, "{diff}");
×
300
                }
301
            }
302
        }
18✔
303

304
        // Otherwise show as - / + lines with consistent indentation
305
        for remove in &self.removals {
18✔
306
            writeln!(
14✔
307
                f,
14✔
308
                "{}",
309
                deleted(&format!("- {}", printer.format_peek(*remove)))
14✔
310
            )?;
×
311
        }
312

313
        for add in &self.additions {
18✔
314
            writeln!(
17✔
315
                f,
17✔
316
                "{}",
317
                inserted(&format!("+ {}", printer.format_peek(*add)))
17✔
318
            )?;
×
319
        }
320

321
        Ok(())
18✔
322
    }
32✔
323
}
324

325
/// Write a sequence of diffs, collapsing Equal diffs into ".. N unchanged items"
326
fn write_diff_sequence(
17✔
327
    f: &mut std::fmt::Formatter<'_>,
17✔
328
    diffs: &[Diff<'_, '_>],
17✔
329
) -> std::fmt::Result {
17✔
330
    let mut i = 0;
17✔
331
    while i < diffs.len() {
58✔
332
        // Count consecutive Equal diffs
333
        let mut equal_count = 0;
41✔
334
        while i + equal_count < diffs.len() {
53✔
335
            if matches!(diffs[i + equal_count], Diff::Equal { .. }) {
53✔
336
                equal_count += 1;
12✔
337
            } else {
12✔
338
                break;
41✔
339
            }
340
        }
341

342
        if equal_count > 0 {
41✔
343
            // Collapse Equal diffs
344
            let label = if equal_count == 1 { "item" } else { "items" };
11✔
345
            writeln!(
11✔
346
                f,
11✔
347
                "{}",
348
                muted(&format!(".. {equal_count} unchanged {label}"))
11✔
349
            )?;
×
350
            i += equal_count;
11✔
351
        } else {
352
            // Show the non-Equal diff
353
            writeln!(f, "{}", diffs[i])?;
30✔
354
            i += 1;
30✔
355
        }
356
    }
357
    Ok(())
17✔
358
}
17✔
359

360
impl<'mem, 'facet> Display for UpdatesGroup<'mem, 'facet> {
361
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49✔
362
        if let Some(update) = &self.0.first {
49✔
363
            update.fmt(f)?;
32✔
364
        }
17✔
365

366
        for (values, update) in &self.0.values {
49✔
367
            write_diff_sequence(f, values)?;
×
368
            update.fmt(f)?;
×
369
        }
370

371
        if let Some(values) = &self.0.last {
49✔
372
            write_diff_sequence(f, values)?;
17✔
373
        }
32✔
374

375
        Ok(())
49✔
376
    }
49✔
377
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc