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 <vector>
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 whose first path segment matches FirstSeg.
156/// Returns pointers in source order. Caller must ensure the vector is non-empty
157/// before use.
158std::vector<const nixf::Binding *>
159collectSiblingBindings(const nixf::Binds &Binds, const std::string &FirstSeg) {
160 std::vector<const nixf::Binding *> Result;
161 for (const auto &Sibling : Binds.bindings()) {
162 if (Sibling->kind() != nixf::Node::NK_Binding)
163 continue;
164
165 const auto &SibBind = static_cast<const nixf::Binding &>(*Sibling);
166 const auto &SibNames = SibBind.path().names();
167
168 if (SibNames.empty() || !SibNames[0]->isStatic())
169 continue;
170
171 if (SibNames[0]->staticName() == FirstSeg)
172 Result.push_back(&SibBind);
173 }
174 return Result;
175}
176
177} // namespace
178
180 const std::string &FileURI, llvm::StringRef Src,
181 std::vector<lspserver::CodeAction> &Actions) {
182 // Find if we're inside a Binding
183 const nixf::Node *BindingNode = PM.upTo(N, nixf::Node::NK_Binding);
184 if (!BindingNode)
185 return;
186
187 const auto &Bind = static_cast<const nixf::Binding &>(*BindingNode);
188 const auto &Names = Bind.path().names();
189
190 // Must have at least 2 path segments (e.g., foo.bar)
191 if (Names.size() < 2)
192 return;
193
194 // All path segments must be static
195 for (const auto &Name : Names) {
196 if (!Name->isStatic())
197 return;
198 }
199
200 // Check parent ExprAttrs is not recursive
201 const nixf::Node *BindsNode = PM.query(Bind);
202 if (!BindsNode || BindsNode->kind() != nixf::Node::NK_Binds)
203 return;
204
205 const nixf::Node *AttrsNode = PM.query(*BindsNode);
206 if (!AttrsNode || AttrsNode->kind() != nixf::Node::NK_ExprAttrs)
207 return;
208
209 const auto &ParentAttrs = static_cast<const nixf::ExprAttrs &>(*AttrsNode);
210 if (ParentAttrs.isRecursive())
211 return;
212
213 const std::string &FirstSeg = Names[0]->staticName();
214 size_t SiblingCount = getSiblingCount(Bind, ParentAttrs);
215
216 if (SiblingCount == 0)
217 return; // Can't pack (dynamic attrs or other conflicts)
218
219 // Helper lambda to generate Pack One action text
220 auto GeneratePackOneText = [&]() -> std::string {
221 std::string NewText;
222 const std::string_view FirstName = Names[0]->src(Src);
223
224 size_t ValueSize = Bind.value() ? Bind.value()->src(Src).size() : 0;
225 NewText.reserve(FirstName.size() + Bind.path().src(Src).size() + ValueSize +
226 15);
227 NewText += FirstName;
228 NewText += " = { ";
229
230 const nixf::LexerCursor RestStart = Names[1]->range().lCur();
231 const nixf::LexerCursor RestEnd = Bind.path().range().rCur();
232
233 // Safety check: ensure valid range to prevent integer underflow
234 if (RestEnd.offset() < RestStart.offset())
235 return "";
236
237 std::string_view RestPath =
238 Src.substr(RestStart.offset(), RestEnd.offset() - RestStart.offset());
239
240 NewText += RestPath;
241 NewText += " = ";
242
243 if (Bind.value()) {
244 NewText += Bind.value()->src(Src);
245 }
246 NewText += "; };";
247 return NewText;
248 };
249
250 if (SiblingCount == 1) {
251 // Single binding - offer simple pack action
252 std::string NewText = GeneratePackOneText();
253 if (NewText.empty())
254 return;
255
256 Actions.emplace_back(createSingleEditAction(
257 "Pack dotted path to nested set",
259 toLSPRange(Src, Bind.range()), std::move(NewText)));
260 } else {
261 // Multiple siblings share the prefix - offer Pack One and bulk pack actions
262 const nixf::SemaAttrs &SA = ParentAttrs.sema();
263 auto It = SA.staticAttrs().find(FirstSeg);
264 if (It == SA.staticAttrs().end())
265 return;
266
267 const nixf::Attribute &Attr = It->second;
268 if (!Attr.value() || Attr.value()->kind() != nixf::Node::NK_ExprAttrs)
269 return;
270
271 const auto &NestedAttrs =
272 static_cast<const nixf::ExprAttrs &>(*Attr.value());
273
274 // Collect all matching sibling bindings in source order. Bulk actions
275 // emit one TextEdit per binding so intermediate non-matching bindings
276 // (e.g. `b.y = 2` in `{ a.x = 1; b.y = 2; a.z = 3; }`) are preserved.
277 const auto &ParentBinds = static_cast<const nixf::Binds &>(*BindsNode);
278 auto Matches = collectSiblingBindings(ParentBinds, FirstSeg);
279 if (Matches.empty())
280 return;
281
282 // Action 1: Pack One - pack only the current binding
283 std::string PackOneText = GeneratePackOneText();
284 if (!PackOneText.empty()) {
285 Actions.emplace_back(createSingleEditAction(
286 "Pack dotted path to nested set",
288 toLSPRange(Src, Bind.range()), std::move(PackOneText)));
289 }
290
291 // Build a vector of TextEdits that replaces the first matching binding
292 // with `PackedText` and deletes every other matching binding in place.
293 auto BuildBulkEdits =
294 [&](std::string PackedText) -> std::vector<lspserver::TextEdit> {
295 std::vector<lspserver::TextEdit> Edits;
296 Edits.reserve(Matches.size());
297 Edits.emplace_back(lspserver::TextEdit{
298 .range = toLSPRange(Src, Matches.front()->range()),
299 .newText = std::move(PackedText),
300 });
301 for (size_t I = 1; I < Matches.size(); ++I) {
302 Edits.emplace_back(lspserver::TextEdit{
303 .range = toLSPRange(Src, Matches[I]->range()),
304 .newText = "",
305 });
306 }
307 return Edits;
308 };
309
310 // Action 2: Shallow Pack All - pack all siblings but only one level deep
311 std::string ShallowText;
312 ShallowText += quoteNixAttrKey(FirstSeg);
313 ShallowText += " = ";
314 generateShallowNestedText(ParentBinds, FirstSeg, Src, ShallowText);
315 ShallowText += ";";
316
317 Actions.emplace_back(createMultiEditAction(
318 "Pack all '" + FirstSeg + "' bindings to nested set",
320 BuildBulkEdits(std::move(ShallowText))));
321
322 // Action 3: Recursive Pack All - fully nest all sibling bindings
323 std::string RecursiveText;
324 RecursiveText += quoteNixAttrKey(FirstSeg);
325 RecursiveText += " = ";
326 generateNestedText(NestedAttrs.sema(), Src, RecursiveText);
327 RecursiveText += ";";
328
329 Actions.emplace_back(createMultiEditAction(
330 "Recursively pack all '" + FirstSeg + "' bindings to nested set",
332 BuildBulkEdits(std::move(RecursiveText))));
333 }
334}
335
336} // 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 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
lspserver::CodeAction createMultiEditAction(const std::string &Title, llvm::StringLiteral Kind, const std::string &FileURI, std::vector< lspserver::TextEdit > Edits)
Create a CodeAction with multiple text edits applied as one.
Definition Utils.cpp:28
std::string quoteNixAttrKey(const std::string &Key)
Quote and escape a Nix attribute key if necessary.
Definition Utils.cpp:102
static const llvm::StringLiteral REFACTOR_REWRITE_KIND