nixd
Loading...
Searching...
No Matches
WithToLet.cpp
Go to the documentation of this file.
1/// \file
2/// \brief Implementation of with-to-let code action.
3
4#include "WithToLet.h"
5#include "Utils.h"
6
7#include "../Convert.h"
8
11
12#include <set>
13
14namespace nixd {
15
16namespace {
17
18/// \brief Check if any variable used by this `with` could also be provided by
19/// a nested (inner) `with` expression.
20///
21/// This uses semantic analysis to detect indirect nested `with` scenarios.
22/// Converting an outer `with` to `let/inherit` is unsafe when variables could
23/// come from inner `with` scopes, because `let` bindings shadow `with` scopes.
24///
25/// \param With The `with` expression to check.
26/// \param VLA The variable lookup analysis containing scope information.
27/// \return true if this `with` has nested `with` scopes that affect its
28/// variables (conversion unsafe), false if it's safe to convert.
29bool hasNestedWithScope(const nixf::ExprWith &With,
30 const nixf::VariableLookupAnalysis &VLA) {
31 // Get the Definition for the `with` keyword to find all variables it provides
32 const nixf::Definition *Def = VLA.toDef(With.kwWith());
33 if (!Def)
34 return false;
35
36 // Check each variable used from this with scope
37 for (const nixf::ExprVar *Var : Def->uses()) {
38 if (!Var)
39 continue;
40
41 // Get all `with` scopes that could provide this variable
42 auto WithScopes = VLA.getWithScopes(*Var);
43
44 // If there are multiple `with` scopes, check if this `with` is not the
45 // innermost one. The WithScopes vector is ordered innermost-to-outermost,
46 // so if our `with` is not the first one, there's a nested `with`.
47 if (WithScopes.size() > 1 && WithScopes.front() != &With) {
48 // This variable could come from a different (inner) `with`,
49 // so converting this outer `with` to `let/inherit` would change semantics
50 return true;
51 }
52 }
53
54 return false;
55}
56
57/// \brief Check if cursor position is on the `with` keyword.
58bool isCursorOnWithKeyword(const nixf::ExprWith &With, const nixf::Node &N) {
59 // The node N should be or contain the `with` keyword
60 const auto &KwWith = With.kwWith();
61 auto KwRange = KwWith.range();
62 auto NRange = N.range();
63
64 // Check if N's range overlaps with the `with` keyword range
65 return NRange.lCur().offset() <= KwRange.rCur().offset() &&
66 NRange.rCur().offset() >= KwRange.lCur().offset();
67}
68
69/// \brief Collect unique variable names used from the with scope.
70/// Returns empty set if the Definition cannot be obtained.
71std::set<std::string>
72collectWithVariables(const nixf::ExprWith &With,
73 const nixf::VariableLookupAnalysis &VLA) {
74 std::set<std::string> VarNames;
75
76 // Get the Definition for the `with` keyword
77 const nixf::Definition *Def = VLA.toDef(With.kwWith());
78 if (!Def)
79 return VarNames;
80
81 // Extract unique variable names from all uses
82 for (const nixf::ExprVar *Var : Def->uses()) {
83 if (Var)
84 VarNames.insert(std::string(Var->id().name()));
85 }
86
87 return VarNames;
88}
89
90/// \brief Generate the let/inherit replacement text.
91/// Format: let inherit (source) var1 var2 ...; in body
92std::string generateLetInherit(const nixf::ExprWith &With,
93 const std::set<std::string> &VarNames,
94 llvm::StringRef Src) {
95 std::string Result;
96
97 // Get source expression text
98 std::string_view SourceExpr;
99 if (With.with())
100 SourceExpr = With.with()->src(Src);
101 else
102 return ""; // Cannot generate without source expression
103
104 // Get body expression text
105 std::string_view BodyExpr;
106 if (With.expr())
107 BodyExpr = With.expr()->src(Src);
108 else
109 return ""; // Cannot generate without body expression
110
111 // Build: let inherit (source) vars; in body
112 Result.reserve(SourceExpr.size() + BodyExpr.size() + VarNames.size() * 10 +
113 30);
114
115 Result += "let inherit (";
116 Result += SourceExpr;
117 Result += ")";
118
119 // Add sorted variable names
120 for (const auto &Name : VarNames) {
121 Result += " ";
122 Result += Name;
123 }
124
125 Result += "; in ";
126 Result += BodyExpr;
127
128 return Result;
129}
130
131} // namespace
132
135 const std::string &FileURI, llvm::StringRef Src,
136 std::vector<lspserver::CodeAction> &Actions) {
137 // Find enclosing ExprWith node
138 const nixf::Node *WithNode = PM.upTo(N, nixf::Node::NK_ExprWith);
139 if (!WithNode)
140 return;
141
142 const auto &With = static_cast<const nixf::ExprWith &>(*WithNode);
143
144 // Check if cursor is on the `with` keyword (not in the body or source expr)
145 if (!isCursorOnWithKeyword(With, N))
146 return;
147
148 // Skip `with` expressions that have nested `with` scopes (direct or
149 // indirect). Converting such a `with` to `let/inherit` can change variable
150 // resolution because `let` bindings shadow inner `with` scopes. This semantic
151 // check handles both direct nesting (with a; with b; x) and indirect nesting
152 // (with a; let y = with b; x; in y). See:
153 // https://github.com/nix-community/nixd/pull/768#discussion_r2681198142
154 if (hasNestedWithScope(With, VLA))
155 return;
156
157 // Collect variables used from this with scope
158 std::set<std::string> VarNames = collectWithVariables(With, VLA);
159
160 // Skip if no variables are used (unused with)
161 // The existing "remove with" quickfix handles this case
162 if (VarNames.empty())
163 return;
164
165 // Generate the replacement text
166 std::string NewText = generateLetInherit(With, VarNames, Src);
167 if (NewText.empty())
168 return;
169
170 // Create the code action
171 Actions.emplace_back(createSingleEditAction(
172 "Convert `with` to `let/inherit`",
174 toLSPRange(Src, With.range()), std::move(NewText)));
175}
176
177} // namespace nixd
Convert between LSP and nixf types.
Shared utilities for code actions.
Code action for converting with expressions to let/inherit.
const std::vector< const ExprVar * > & uses() const
const Misc & kwWith() const
Definition Expr.h:174
Expr * with() const
Definition Expr.h:176
Expr * expr() const
Definition Expr.h:177
LexerCursor lCur() const
Definition Range.h:116
std::size_t offset() const
Offset in the source file, starting from 0.
Definition Range.h:102
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 Definition * toDef(const Node &N) const
Get definition record for some name.
std::vector< const ExprWith * > getWithScopes(const ExprVar &Var) const
Get all with expressions that could provide the binding for a variable.
void addWithToLetAction(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, const nixf::VariableLookupAnalysis &VLA, const std::string &FileURI, llvm::StringRef Src, std::vector< lspserver::CodeAction > &Actions)
Add code action to convert with expression to let/inherit.
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
static const llvm::StringLiteral REFACTOR_REWRITE_KIND