1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use rnix::TextRange;
use crate::follows::{AttrPath, Segment, strip_outer_quotes};
/// A single flake input declaration.
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub struct Input {
pub(crate) id: Segment,
pub(crate) flake: bool,
/// Stored unquoted. Quoting is re-applied at write-back time.
pub(crate) url: String,
pub(crate) follows: Vec<Follows>,
pub range: Range,
}
/// Source byte range, half-open: `[start, end)`.
#[derive(Debug, Default, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub struct Range {
pub start: usize,
pub end: usize,
}
impl Range {
pub fn from_text_range(text_range: TextRange) -> Self {
Self {
start: text_range.start().into(),
end: text_range.end().into(),
}
}
/// True if the range is the default (zero) range, used as a sentinel for
/// inputs without a write-back location.
pub fn is_empty(&self) -> bool {
self.start == 0 && self.end == 0
}
}
/// A `follows` declaration on an [`Input`].
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub enum Follows {
/// A nested input redirected to another input via `follows = "..."`.
///
/// `path` is the nested-input chain relative to the owning [`Input`] and
/// does not include the owner's id segment. `target` is the right-hand
/// side of the `follows = "..."`; `None` represents the empty-string
/// form `follows = ""`, the in-flake equivalent of the lockfile's
/// [`crate::lock::Input::Indirect`]`(None)` (an `inputs.X = []` entry).
///
/// - `inputs.crane.inputs.nixpkgs.follows = "nixpkgs"` is stored on
/// `crane` as `Indirect { path: ["nixpkgs"], target: Some(["nixpkgs"]) }`.
/// - `inputs.neovim.inputs.nixvim.inputs.flake-parts.follows =
/// "flake-parts"` is stored on `neovim` as `Indirect { path:
/// ["nixvim", "flake-parts"], target: Some(["flake-parts"]) }`.
/// - `inputs.nix.inputs.flake-compat.follows = ""` is stored on `nix`
/// as `Indirect { path: ["flake-compat"], target: None }`.
Indirect {
path: AttrPath,
target: Option<AttrPath>,
},
/// A nested input declared inline with its own URL.
Direct(String, Input),
}
impl Input {
pub(crate) fn new(name: Segment) -> Self {
Self {
id: name,
flake: true,
url: String::new(),
follows: Vec::new(),
range: Range::default(),
}
}
/// Build an [`Input`] with `id`, `url`, and the range derived from
/// `text_range`. Surrounding double-quotes on `url` are stripped.
pub(crate) fn with_url(id: Segment, url: String, text_range: TextRange) -> Self {
Self {
id,
flake: true,
url: strip_outer_quotes(&url).to_string(),
follows: Vec::new(),
range: Range::from_text_range(text_range),
}
}
pub fn id(&self) -> &Segment {
&self.id
}
pub fn url(&self) -> &str {
self.url.as_ref()
}
pub fn follows(&self) -> &Vec<Follows> {
self.follows.as_ref()
}
/// True if the URL can be rewritten in place. False for synthetic inputs
/// without a known source range.
pub fn has_editable_url(&self) -> bool {
!self.url.is_empty() && !self.range.is_empty()
}
/// Append an `Indirect` follows entry and re-normalize the follows vec
/// (sort + dedup). Walker insertion sites maintain this invariant so
/// callers downstream (validate, follows-graph, snapshots) see one
/// canonical ordering.
pub(crate) fn push_indirect_follows(&mut self, path: AttrPath, target: Option<AttrPath>) {
self.follows.push(Follows::Indirect { path, target });
self.follows.sort();
self.follows.dedup();
}
}