nixd
Loading...
Searching...
No Matches
PackAttrs.cpp
Go to the documentation of this file.
1/// \file
2/// \brief Implementation of pack dotted paths code action.
3
4#include "PackAttrs.h"
5#include "Utils.h"
6
7#include "../Convert.h"
8
10
11#include <optional>
12
13namespace nixd {
14
15namespace {
16
17/// \brief Maximum recursion depth for nested text generation.
18/// Prevents stack overflow on maliciously crafted deeply nested inputs.
19constexpr size_t MaxNestedDepth = 100;
20
21/// \brief Get the number of sibling bindings sharing the same first path
22/// segment. Uses SemaAttrs to count nested attributes for the first segment.
23/// Returns 0 if the segment is not found or has conflicts (non-path binding).
24size_t getSiblingCount(const nixf::Binding &Bind,
25 const nixf::ExprAttrs &ParentAttrs) {
26 const auto &Names = Bind.path().names();
27 if (Names.empty() || !Names[0]->isStatic())
28 return 0;
29
30 const std::string &FirstSeg = Names[0]->staticName();
31 const nixf::SemaAttrs &SA = ParentAttrs.sema();
32
33 auto It = SA.staticAttrs().find(FirstSeg);
34 if (It == SA.staticAttrs().end())
35 return 0;
36
37 const nixf::Attribute &Attr = It->second;
38
39 // Check if value is a nested ExprAttrs (path was desugared)
40 if (!Attr.value() || Attr.value()->kind() != nixf::Node::NK_ExprAttrs)
41 return 0; // Non-ExprAttrs value = conflict with non-path binding
42
43 const auto &NestedAttrs = static_cast<const nixf::ExprAttrs &>(*Attr.value());
44 const nixf::SemaAttrs &NestedSA = NestedAttrs.sema();
45
46 // Return 0 if there are dynamic attrs (can't safely pack)
47 if (!NestedSA.dynamicAttrs().empty())
48 return 0;
49
50 return NestedSA.staticAttrs().size();
51}
52
53/// \brief Recursively generate nested attribute set text from SemaAttrs.
54/// This produces the fully packed/nested form of attributes.
55/// \param Depth Current recursion depth (for safety limit)
56void generateNestedText(const nixf::SemaAttrs &SA, llvm::StringRef Src,
57 std::string &Out, size_t Depth = 0) {
58 // Safety check: prevent stack overflow from deeply nested structures
59 if (Depth > MaxNestedDepth) {
60 Out += "{ /* max depth exceeded */ }";
61 return;
62 }
63
64 Out += "{ ";
65 bool First = true;
66 for (const auto &[Key, Attr] : SA.staticAttrs()) {
67 if (!First)
68 Out += " ";
69 First = false;
70
71 // Output the key, quoting if necessary
72 Out += quoteNixAttrKey(Key);
73 Out += " = ";
74
75 // Check if value is a nested ExprAttrs that needs recursive generation
76 if (Attr.value() && Attr.value()->kind() == nixf::Node::NK_ExprAttrs) {
77 const auto &NestedAttrs =
78 static_cast<const nixf::ExprAttrs &>(*Attr.value());
79 const nixf::SemaAttrs &NestedSA = NestedAttrs.sema();
80
81 // If all nested attrs come from dotted paths (no inherit, no dynamic),
82 // we can generate recursively
83 if (NestedSA.dynamicAttrs().empty() && !Attr.fromInherit()) {
84 generateNestedText(NestedSA, Src, Out, Depth + 1);
85 } else {
86 // Use original source text
87 Out += Attr.value()->src(Src);
88 }
89 } else if (Attr.value()) {
90 Out += Attr.value()->src(Src);
91 }
92 Out += ";";
93 }
94 Out += " }";
95}
96
97/// \brief Generate shallow nested attribute set text from original bindings.
98/// Unlike generateNestedText, this only expands one level and preserves
99/// remaining dotted paths as-is by extracting from original source.
100/// \param Binds The original Binds node containing all bindings
101/// \param FirstSeg The first path segment to match (e.g., "foo" for "foo.bar")
102/// \param Src The source text
103/// \param Out Output string to append to
104void generateShallowNestedText(const nixf::Binds &Binds,
105 const std::string &FirstSeg, llvm::StringRef Src,
106 std::string &Out) {
107 Out += "{ ";
108 bool First = true;
109
110 for (const auto &Child : Binds.bindings()) {
111 if (Child->kind() != nixf::Node::NK_Binding)
112 continue;
113
114 const auto &SibBind = static_cast<const nixf::Binding &>(*Child);
115 const auto &Names = SibBind.path().names();
116
117 // Only process bindings that match the first segment
118 if (Names.empty() || !Names[0]->isStatic() ||
119 Names[0]->staticName() != FirstSeg)
120 continue;
121
122 if (!First)
123 Out += " ";
124 First = false;
125
126 if (Names.size() == 1) {
127 // Single segment path (e.g., just "foo") - shouldn't happen in bulk pack
128 // but handle it gracefully by using value directly
129 Out += quoteNixAttrKey(Names[0]->staticName());
130 Out += " = ";
131 if (SibBind.value()) {
132 Out += SibBind.value()->src(Src);
133 }
134 } else {
135 // Multi-segment path - extract rest of path from source
136 // e.g., "foo.bar.x = 1" -> "bar.x = 1"
137 const nixf::LexerCursor RestStart = Names[1]->range().lCur();
138 const nixf::LexerCursor PathEnd = SibBind.path().range().rCur();
139
140 if (PathEnd.offset() >= RestStart.offset()) {
141 std::string_view RestPath = Src.substr(
142 RestStart.offset(), PathEnd.offset() - RestStart.offset());
143 Out += RestPath;
144 Out += " = ";
145 if (SibBind.value()) {
146 Out += SibBind.value()->src(Src);
147 }
148 }
149 }
150 Out += ";";
151 }
152 Out += " }";
153}
154
155/// \brief Find all sibling bindings that share the same first path segment.
156/// Returns the range covering all such bindings, or nullopt if not applicable.
157std::optional<nixf::LexerCursorRange>
158findSiblingBindingsRange(const nixf::Binding &Bind, const nixf::Binds &Binds,
159 const std::string &FirstSeg) {
160 nixf::LexerCursor Start = Bind.range().lCur();
161 nixf::LexerCursor End = Bind.range().rCur();
162
163 for (const auto &Sibling : Binds.bindings()) {
164 if (Sibling->kind() != nixf::Node::NK_Binding)
165 continue;
166
167 const auto &SibBind = static_cast<const nixf::Binding &>(*Sibling);
168 const auto &SibNames = SibBind.path().names();
169
170 if (SibNames.empty() || !SibNames[0]->isStatic())
171 continue;
172
173 if (SibNames[0]->staticName() == FirstSeg) {
174 // Expand range to include this sibling
175 if (SibBind.range().lCur().offset() < Start.offset())
176 Start = SibBind.range().lCur();
177 if (SibBind.range().rCur().offset() > End.offset())
178 End = SibBind.range().rCur();
179 }
180 }
181
182 return nixf::LexerCursorRange{Start, End};
183}
184
185} // namespace
186
188 const std::string &FileURI, llvm::StringRef Src,
189 std::vector<lspserver::CodeAction> &Actions) {
190 // Find if we're inside a Binding
191 const nixf::Node *BindingNode = PM.upTo(N, nixf::Node::NK_Binding);
192 if (!BindingNode)
193 return;
194
195 const auto &Bind = static_cast<const nixf::Binding &>(*BindingNode);
196 const auto &Names = Bind.path().names();
197
198 // Must have at least 2 path segments (e.g., foo.bar)
199 if (Names.size() < 2)
200 return;
201
202 // All path segments must be static
203 for (const auto &Name : Names) {
204 if (!Name->isStatic())
205 return;
206 }
207
208 // Check parent ExprAttrs is not recursive
209 const nixf::Node *BindsNode = PM.query(Bind);
210 if (!BindsNode || BindsNode->kind() != nixf::Node::NK_Binds)
211 return;
212
213 const nixf::Node *AttrsNode = PM.query(*BindsNode);
214 if (!AttrsNode || AttrsNode->kind() != nixf::Node::NK_ExprAttrs)
215 return;
216
217 const auto &ParentAttrs = static_cast<const nixf::ExprAttrs &>(*AttrsNode);
218 if (ParentAttrs.isRecursive())
219 return;
220
221 const std::string &FirstSeg = Names[0]->staticName();
222 size_t SiblingCount = getSiblingCount(Bind, ParentAttrs);
223
224 if (SiblingCount == 0)
225 return; // Can't pack (dynamic attrs or other conflicts)
226
227 // Helper lambda to generate Pack One action text
228 auto GeneratePackOneText = [&]() -> std::string {
229 std::string NewText;
230 const std::string_view FirstName = Names[0]->src(Src);
231
232 size_t ValueSize = Bind.value() ? Bind.value()->src(Src).size() : 0;
233 NewText.reserve(FirstName.size() + Bind.path().src(Src).size() + ValueSize +
234 15);
235 NewText += FirstName;
236 NewText += " = { ";
237
238 const nixf::LexerCursor RestStart = Names[1]->range().lCur();
239 const nixf::LexerCursor RestEnd = Bind.path().range().rCur();
240
241 // Safety check: ensure valid range to prevent integer underflow
242 if (RestEnd.offset() < RestStart.offset())
243 return "";
244
245 std::string_view RestPath =
246 Src.substr(RestStart.offset(), RestEnd.offset() - RestStart.offset());
247
248 NewText += RestPath;
249 NewText += " = ";
250
251 if (Bind.value()) {
252 NewText += Bind.value()->src(Src);
253 }
254 NewText += "; };";
255 return NewText;
256 };
257
258 if (SiblingCount == 1) {
259 // Single binding - offer simple pack action
260 std::string NewText = GeneratePackOneText();
261 if (NewText.empty())
262 return;
263
264 Actions.emplace_back(createSingleEditAction(
265 "Pack dotted path to nested set",
267 toLSPRange(Src, Bind.range()), std::move(NewText)));
268 } else {
269 // Multiple siblings share the prefix - offer Pack One and bulk pack actions
270 const nixf::SemaAttrs &SA = ParentAttrs.sema();
271 auto It = SA.staticAttrs().find(FirstSeg);
272 if (It == SA.staticAttrs().end())
273 return;
274
275 const nixf::Attribute &Attr = It->second;
276 if (!Attr.value() || Attr.value()->kind() != nixf::Node::NK_ExprAttrs)
277 return;
278
279 const auto &NestedAttrs =
280 static_cast<const nixf::ExprAttrs &>(*Attr.value());
281
282 // Find the range covering all sibling bindings (needed for bulk actions)
283 const auto &ParentBinds = static_cast<const nixf::Binds &>(*BindsNode);
284 auto BulkRange = findSiblingBindingsRange(Bind, ParentBinds, FirstSeg);
285 if (!BulkRange)
286 return;
287
288 // Action 1: Pack One - pack only the current binding
289 std::string PackOneText = GeneratePackOneText();
290 if (!PackOneText.empty()) {
291 Actions.emplace_back(createSingleEditAction(
292 "Pack dotted path to nested set",
294 toLSPRange(Src, Bind.range()), std::move(PackOneText)));
295 }
296
297 // Action 2: Shallow Pack All - pack all siblings but only one level deep
298 std::string ShallowText;
299 ShallowText += quoteNixAttrKey(FirstSeg);
300 ShallowText += " = ";
301 generateShallowNestedText(ParentBinds, FirstSeg, Src, ShallowText);
302 ShallowText += ";";
303
304 Actions.emplace_back(createSingleEditAction(
305 "Pack all '" + FirstSeg + "' bindings to nested set",
307 toLSPRange(Src, *BulkRange), std::move(ShallowText)));
308
309 // Action 3: Recursive Pack All - fully nest all sibling bindings
310 std::string RecursiveText;
311 RecursiveText += quoteNixAttrKey(FirstSeg);
312 RecursiveText += " = ";
313 generateNestedText(NestedAttrs.sema(), Src, RecursiveText);
314 RecursiveText += ";";
315
316 Actions.emplace_back(createSingleEditAction(
317 "Recursively pack all '" + FirstSeg + "' bindings to nested set",
319 toLSPRange(Src, *BulkRange), std::move(RecursiveText)));
320 }
321}
322
323} // namespace nixd
Convert between LSP and nixf types.
Code action for packing dotted attribute paths into nested sets.
Shared utilities for code actions.
const std::vector< std::shared_ptr< AttrName > > & names() const
Definition Attrs.h:100
Expr * value() const
Definition Attrs.h:219
bool fromInherit() const
Definition Attrs.h:223
const AttrPath & path() const
Definition Attrs.h:131
const std::shared_ptr< Expr > & value() const
Definition Attrs.h:136
const std::vector< std::shared_ptr< Node > > & bindings() const
Definition Attrs.h:180
bool isRecursive() const
Definition Attrs.h:287
const SemaAttrs & sema() const
Definition Attrs.h:289
LexerCursor lCur() const
Definition Range.h:116
LexerCursor rCur() const
Definition Range.h:117
A point in the source file.
Definition Range.h:57
std::size_t offset() const
Offset in the source file, starting from 0.
Definition Range.h:102
NodeKind kind() const
Definition Basic.h:34
std::string_view src(std::string_view Src) const
Definition Basic.h:63
LexerCursorRange range() const
Definition Basic.h:35
const Node * upTo(const Node &N, Node::NodeKind Kind) const
Search up until some kind of node is found.
Definition ParentMap.cpp:27
const Node * query(const Node &N) const
Definition ParentMap.cpp:13
Attribute set after deduplication.
Definition Attrs.h:236
const std::vector< Attribute > & dynamicAttrs() const
Dynamic attributes, require evaluation to get the key.
Definition Attrs.h:264
const std::map< std::string, Attribute > & staticAttrs() const
Static attributes, do not require evaluation to get the key.
Definition Attrs.h:257
void addPackAttrsAction(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, const std::string &FileURI, llvm::StringRef Src, std::vector< lspserver::CodeAction > &Actions)
Add pack action for dotted attribute paths.
lspserver::CodeAction createSingleEditAction(const std::string &Title, llvm::StringLiteral Kind, const std::string &FileURI, const lspserver::Range &EditRange, std::string NewText)
Create a CodeAction with a single text edit.
Definition Utils.cpp:10
lspserver::Range toLSPRange(llvm::StringRef Code, const nixf::LexerCursorRange &R)
Definition Convert.cpp:40
std::string quoteNixAttrKey(const std::string &Key)
Quote and escape a Nix attribute key if necessary.
Definition Utils.cpp:89
static const llvm::StringLiteral REFACTOR_REWRITE_KIND