nixd
Loading...
Searching...
No Matches
VariableLookup.cpp
Go to the documentation of this file.
7
8#include <set>
9
10using namespace nixf;
11
12namespace {
13
14std::set<std::string> Constants{
15 "true", "false", "null",
16 "__currentTime", "__currentSystem", "__nixVersion",
17 "__storeDir", "__langVersion", "__importNative",
18 "__traceVerbose", "__nixPath", "derivation",
19};
20
21/// Builder a map of definitions. If there are something overlapped, maybe issue
22/// a diagnostic.
23class DefBuilder {
25 std::vector<Diagnostic> &Diags;
26
27 std::shared_ptr<Definition> addSimple(std::string Name, const Node *Entry,
29 assert(!Def.contains(Name));
30 auto NewDef = std::make_shared<Definition>(Entry, Source);
31 Def.insert({std::move(Name), NewDef});
32 return NewDef;
33 }
34
35public:
36 DefBuilder(std::vector<Diagnostic> &Diags) : Diags(Diags) {}
37
38 void addBuiltin(std::string Name) {
39 // Don't need to record def map for builtins.
40 auto _ = addSimple(std::move(Name), nullptr, Definition::DS_Builtin);
41 }
42
43 [[nodiscard("Record ToDef Map!")]] std::shared_ptr<Definition>
44 add(std::string Name, const Node *Entry, Definition::DefinitionSource Source,
45 bool IsInheritFromBuiltin) {
46 auto PrimOpLookup = lookupGlobalPrimOpInfo(Name);
47 if (PrimOpLookup == PrimopLookupResult::Found && !IsInheritFromBuiltin) {
48 // Overriding a builtin primop is discouraged.
49 Diagnostic &D =
50 Diags.emplace_back(Diagnostic::DK_PrimOpOverridden, Entry->range());
51 D << Name;
52 }
53
54 // Lookup constants
55 if (Constants.contains(Name)) {
56 Diagnostic &D =
57 Diags.emplace_back(Diagnostic::DK_ConstantOverridden, Entry->range());
58 D << Name;
59 }
60
61 return addSimple(std::move(Name), Entry, Source);
62 }
63
64 EnvNode::DefMap finish() { return std::move(Def); }
65};
66
67/// Special check for inherited from builtins attribute.
68/// e.g. inherit (builtins) foo bar;
69/// Returns true if the attribute is inherited from builtins.
70///
71/// Suppress warnings for overriding primops in this case.
72bool checkInheritedFromBuiltin(const Attribute &Attr) {
74 return false;
75
76 assert(Attr.value() &&
77 "select expr desugared from inherit should not be null");
78 assert(Attr.value()->kind() == Node::NK_ExprSelect &&
79 "desugared inherited from should be a select expr");
80 const auto &Select = static_cast<const ExprSelect &>(*Attr.value());
81 if (Select.expr().kind() == Node::NK_ExprVar) {
82 const auto &Var = static_cast<const ExprVar &>(Select.expr());
83 return Var.id().name() == "builtins";
84 }
85 return false;
86}
87
88bool isBuiltinConstant(const std::string &Name) {
89 if (Name.starts_with("_"))
90 return false;
91 return Constants.contains(Name) || Constants.contains("__" + Name);
92}
93
94} // namespace
95
96bool EnvNode::isLive() const {
97 for (const auto &[_, D] : Defs) {
98 if (!D->uses().empty())
99 return true;
100 }
101 return false;
102}
103
104void VariableLookupAnalysis::emitEnvLivenessWarning(
105 const std::shared_ptr<EnvNode> &NewEnv) {
106 for (const auto &[Name, Def] : NewEnv->defs()) {
107 // If the definition comes from lambda arg, omit the diagnostic
108 // because there is no elegant way to "fix" this trivially & keep
109 // the lambda signature.
110 if (Def->source() == Definition::DS_LambdaArg)
111 continue;
112 // Ignore builtins usage.
113 if (!Def->syntax())
114 continue;
115 if (Def->uses().empty()) {
116 Diagnostic::DiagnosticKind Kind = [&]() {
117 switch (Def->source()) {
119 return Diagnostic::DK_UnusedDefLet;
121 return Diagnostic::DK_UnusedDefLambdaNoArg_Formal;
123 return Diagnostic::DK_UnusedDefLambdaWithArg_Formal;
125 return Diagnostic::DK_UnusedDefLambdaWithArg_Arg;
126 default:
127 assert(false && "liveness diagnostic encountered an unknown source!");
128 __builtin_unreachable();
129 }
130 }();
131 Diagnostic &D = Diags.emplace_back(Kind, Def->syntax()->range());
132 D << Name;
134
135 // Add fix for unused let bindings
136 if (Def->source() == Definition::DS_Let) {
137 const Node *LetNode = NewEnv->syntax();
138 if (LetNode && LetNode->kind() == Node::NK_ExprLet) {
139 const auto &Let = static_cast<const ExprLet &>(*LetNode);
140 if (Let.binds()) {
141 for (const auto &BindNode : Let.binds()->bindings()) {
142 // Skip non-Binding nodes (Inherit handled separately)
143 if (BindNode->kind() != Node::NK_Binding)
144 continue;
145
146 const auto &Bind = static_cast<const Binding &>(*BindNode);
147 const auto &PathNames = Bind.path().names();
148 if (PathNames.empty())
149 continue;
150
151 const auto &FirstName = PathNames[0];
152 if (!FirstName)
153 continue;
154
155 // Match by comparing first attrname range with Def->syntax()
156 // range
157 if (D.range().lCur() == FirstName->range().lCur() &&
158 D.range().rCur() == FirstName->range().rCur()) {
159 D.fix("remove unused binding")
160 .edit(TextEdit::mkRemoval(Bind.range()));
161 break;
162 }
163 }
164 }
165 }
166 }
167 }
168 }
169}
170
171void VariableLookupAnalysis::lookupVar(const ExprVar &Var,
172 const std::shared_ptr<EnvNode> &Env) {
173 const auto &Name = Var.id().name();
174 const auto *CurEnv = Env.get();
175 std::shared_ptr<Definition> Def;
176 std::vector<const EnvNode *> WithEnvs;
177 for (; CurEnv; CurEnv = CurEnv->parent()) {
178 if (CurEnv->defs().contains(Name)) {
179 Def = CurEnv->defs().at(Name);
180 break;
181 }
182 // Find all nested "with" expression, variables potentially come from those.
183 // For example
184 // with lib;
185 // with builtins;
186 // generators <--- this variable may come from "lib" | "builtins"
187 //
188 // We cannot determine where it precisely come from, thus mark all Envs
189 // alive.
190 if (CurEnv->isWith()) {
191 WithEnvs.emplace_back(CurEnv);
192 }
193 }
194
195 if (Def) {
196 Def->usedBy(Var);
197 Results.insert({&Var, LookupResult{LookupResultKind::Defined, Def}});
198 } else if (!WithEnvs.empty()) { // comes from enclosed "with" expressions.
199 // Collect all `with` expressions that could provide this variable's
200 // binding. This is stored for later queries (e.g., by code actions that
201 // need to determine if converting a `with` to `let/inherit` is safe).
202 std::vector<const ExprWith *> WithScopes;
203 for (const auto *WithEnv : WithEnvs) {
204 Def = WithDefs.at(WithEnv->syntax());
205 Def->usedBy(Var);
206 WithScopes.push_back(static_cast<const ExprWith *>(WithEnv->syntax()));
207 }
208 VarWithScopes.insert({&Var, std::move(WithScopes)});
209 Results.insert({&Var, LookupResult{LookupResultKind::FromWith, Def}});
210 } else {
211 // Check if this is a primop.
212 switch (lookupGlobalPrimOpInfo(Name)) {
214 assert(false && "primop name should be defined");
215 break;
217 Diagnostic &D =
218 Diags.emplace_back(Diagnostic::DK_PrimOpNeedsPrefix, Var.range());
219 D.fix("use `builtins.` prefix")
220 .edit(TextEdit::mkInsertion(Var.range().lCur(), "builtins."));
221 Results.insert(
222 {&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
223 break;
224 }
226 // Otherwise, this variable is undefined.
227 Results.insert(
228 {&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
229 Diagnostic &Diag =
230 Diags.emplace_back(Diagnostic::DK_UndefinedVariable, Var.range());
231 Diag << Var.id().name();
232 break;
233 }
234 }
235}
236
237void VariableLookupAnalysis::dfs(const ExprLambda &Lambda,
238 const std::shared_ptr<EnvNode> &Env) {
239 // Early exit for in-complete lambda.
240 if (!Lambda.body())
241 return;
242
243 // Create a new EnvNode, as lambdas may have formal & arg.
244 DefBuilder DBuilder(Diags);
245 assert(Lambda.arg());
246 const LambdaArg &Arg = *Lambda.arg();
247
248 // foo: body
249 // ^~~<------- add function argument.
250 if (Arg.id()) {
251 if (!Arg.formals()) {
252 ToDef.insert_or_assign(Arg.id(),
253 DBuilder.add(Arg.id()->name(), Arg.id(),
255 /*IsInheritFromBuiltin=*/false));
256 // Function arg cannot duplicate to it's formal.
257 // If it this unluckily happens, we would like to skip this definition.
258 } else if (!Arg.formals()->dedup().contains(Arg.id()->name())) {
259 ToDef.insert_or_assign(Arg.id(),
260 DBuilder.add(Arg.id()->name(), Arg.id(),
262 /*IsInheritFromBuiltin=*/false));
263 }
264 }
265
266 // { foo, bar, ... } : body
267 // ^~~~~~~~~<-------------- add function formals.
268
269 // This section differentiates between formal parameters with an argument and
270 // without. Example:
271 //
272 // { foo }@arg : use arg
273 //
274 // In this case, the definition of `foo` is not used directly; however, it
275 // might be accessed via arg.foo. Therefore, the severity of an unused formal
276 // parameter is reduced in this scenario.
277 if (Arg.formals()) {
278 for (const auto &[Name, Formal] : Arg.formals()->dedup()) {
282 ToDef.insert_or_assign(Formal->id(),
283 DBuilder.add(Name, Formal->id(), Source,
284 /*IsInheritFromBuiltin=*/false));
285 }
286 }
287
288 auto NewEnv = std::make_shared<EnvNode>(Env, DBuilder.finish(), &Lambda);
289
290 if (Arg.formals()) {
291 for (const auto &Formal : Arg.formals()->members()) {
292 if (const Expr *Def = Formal->defaultExpr()) {
293 dfs(*Def, NewEnv);
294 }
295 }
296 }
297
298 dfs(*Lambda.body(), NewEnv);
299
300 emitEnvLivenessWarning(NewEnv);
301}
302
303void VariableLookupAnalysis::dfsDynamicAttrs(
304 const std::vector<Attribute> &DynamicAttrs,
305 const std::shared_ptr<EnvNode> &Env) {
306 for (const auto &Attr : DynamicAttrs) {
307 if (!Attr.value())
308 continue;
309 dfs(Attr.key(), Env);
310 dfs(*Attr.value(), Env);
311 }
312}
313
314std::shared_ptr<EnvNode> VariableLookupAnalysis::dfsAttrs(
315 const SemaAttrs &SA, const std::shared_ptr<EnvNode> &Env,
316 const Node *Syntax, Definition::DefinitionSource Source) {
317 if (SA.isRecursive()) {
318 // rec { }, or let ... in ...
319 DefBuilder DB(Diags);
320 // For each static names, create a name binding.
321 for (const auto &[Name, Attr] : SA.staticAttrs()) {
322 ToDef.insert_or_assign(
323 &Attr.key(),
324 DB.add(Name, &Attr.key(), Source, checkInheritedFromBuiltin(Attr)));
325 }
326
327 auto NewEnv = std::make_shared<EnvNode>(Env, DB.finish(), Syntax);
328
329 for (const auto &[_, Attr] : SA.staticAttrs()) {
330 if (!Attr.value())
331 continue;
334 dfs(*Attr.value(), NewEnv);
335 } else {
336 assert(Attr.kind() == Attribute::AttributeKind::Inherit);
337 dfs(*Attr.value(), Env);
338 }
339 }
340
341 dfsDynamicAttrs(SA.dynamicAttrs(), NewEnv);
342 return NewEnv;
343 }
344
345 // Non-recursive. Dispatch nested node with old Env
346 for (const auto &[_, Attr] : SA.staticAttrs()) {
347 if (!Attr.value())
348 continue;
349 dfs(*Attr.value(), Env);
350 }
351
352 dfsDynamicAttrs(SA.dynamicAttrs(), Env);
353 return Env;
354};
355
356void VariableLookupAnalysis::dfs(const ExprAttrs &Attrs,
357 const std::shared_ptr<EnvNode> &Env) {
358 const SemaAttrs &SA = Attrs.sema();
359 std::shared_ptr<EnvNode> NewEnv =
360 dfsAttrs(SA, Env, &Attrs, Definition::DS_Rec);
361 if (NewEnv != Env) {
362 assert(Attrs.isRecursive() &&
363 "NewEnv must be created for recursive attrset");
364 if (!NewEnv->isLive()) {
365 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_ExtraRecursive,
366 Attrs.rec()->range());
367 D.fix("remove `rec` keyword")
368 .edit(TextEdit::mkRemoval(Attrs.rec()->range()));
370 }
371 }
372}
373
374void VariableLookupAnalysis::dfs(const ExprLet &Let,
375 const std::shared_ptr<EnvNode> &Env) {
376
377 // Obtain the env object suitable for "in" expression.
378 auto GetLetEnv = [&Env, &Let, this]() -> std::shared_ptr<EnvNode> {
379 // This is an empty let ... in ... expr, definitely anti-pattern in
380 // nix language. Create a trivial env and return.
381 if (!Let.attrs()) {
382 auto NewEnv = std::make_shared<EnvNode>(Env, EnvNode::DefMap{}, &Let);
383 return NewEnv;
384 }
385
386 // If there are some attributes actually, create a new env.
387 const SemaAttrs &SA = Let.attrs()->sema();
388 assert(SA.isRecursive() && "let ... in ... attrset must be recursive");
389 checkLetInheritBuiltins(SA);
390 return dfsAttrs(SA, Env, &Let, Definition::DS_Let);
391 };
392
393 auto LetEnv = GetLetEnv();
394
395 if (Let.expr())
396 dfs(*Let.expr(), LetEnv);
397 emitEnvLivenessWarning(LetEnv);
398}
399
400void VariableLookupAnalysis::trivialDispatch(
401 const Node &Root, const std::shared_ptr<EnvNode> &Env) {
402 for (const Node *Ch : Root.children()) {
403 if (!Ch)
404 continue;
405 dfs(*Ch, Env);
406 }
407}
408
409void VariableLookupAnalysis::dfs(const ExprWith &With,
410 const std::shared_ptr<EnvNode> &Env) {
411 auto NewEnv = std::make_shared<EnvNode>(Env, EnvNode::DefMap{}, &With);
412 if (!WithDefs.contains(&With)) {
413 auto NewDef =
414 std::make_shared<Definition>(&With.kwWith(), Definition::DS_With);
415 ToDef.insert_or_assign(&With.kwWith(), NewDef);
416 WithDefs.insert_or_assign(&With, NewDef);
417 }
418
419 if (With.with())
420 dfs(*With.with(), Env);
421
422 if (With.expr())
423 dfs(*With.expr(), NewEnv);
424
425 if (WithDefs.at(&With)->uses().empty()) {
426 Diagnostic &D =
427 Diags.emplace_back(Diagnostic::DK_ExtraWith, With.kwWith().range());
428 Fix &F = D.fix("remove `with` expression")
430 if (With.tokSemi())
432 if (With.with())
433 F.edit(TextEdit::mkRemoval(With.with()->range()));
434 }
435}
436
437void VariableLookupAnalysis::checkBuiltins(const ExprSelect &Sel) {
438 if (!Sel.path())
439 return;
440
441 // Don't emit diagnostics for select expressions desugared from inherit.
442 if (Sel.desugaredFrom())
443 return;
444
445 if (Sel.expr().kind() != Node::NK_ExprVar)
446 return;
447
448 const auto &Builtins = static_cast<const ExprVar &>(Sel.expr());
449 if (Builtins.id().name() != "builtins")
450 return;
451
452 const auto &AP = *Sel.path();
453
454 if (AP.names().size() != 1)
455 return;
456
457 AttrName &First = *AP.names()[0];
458 if (!First.isStatic())
459 return;
460
461 const auto &Name = First.staticName();
462
463 switch (lookupGlobalPrimOpInfo(Name)) {
465 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_PrimOpRemovablePrefix,
466 Builtins.range());
467 Fix &F =
468 D.fix("remove `builtins.` prefix")
469 .edit(TextEdit::mkRemoval(Builtins.range())); // remove `builtins`
470
471 if (Sel.dot()) {
472 // remove the dot also.
473 F.edit(TextEdit::mkRemoval(Sel.dot()->range()));
474 }
475 return;
476 }
478 return;
480 if (!isBuiltinConstant(Name)) {
481 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_PrimOpUnknown,
482 AP.names()[0]->range());
483 D << Name;
484 return;
485 }
486 }
487}
488
489void VariableLookupAnalysis::checkLetInheritBuiltins(const SemaAttrs &SA) {
490 for (const auto &[Name, Attr] : SA.staticAttrs()) {
491 if (!checkInheritedFromBuiltin(Attr))
492 continue;
493
494 // Check if the inherited name is a prelude builtin
496 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_PrimOpRemovablePrefix,
497 Attr.key().range());
498 D.fix("remove unnecessary inherit")
499 .edit(TextEdit::mkRemoval(Attr.key().range()));
500 }
501 }
502}
503
504void VariableLookupAnalysis::dfs(const Node &Root,
505 const std::shared_ptr<EnvNode> &Env) {
506 Envs.insert({&Root, Env});
507 switch (Root.kind()) {
508 case Node::NK_ExprVar: {
509 const auto &Var = static_cast<const ExprVar &>(Root);
510 lookupVar(Var, Env);
511 break;
512 }
513 case Node::NK_ExprLambda: {
514 const auto &Lambda = static_cast<const ExprLambda &>(Root);
515 dfs(Lambda, Env);
516 break;
517 }
518 case Node::NK_ExprAttrs: {
519 const auto &Attrs = static_cast<const ExprAttrs &>(Root);
520 dfs(Attrs, Env);
521 break;
522 }
523 case Node::NK_ExprLet: {
524 const auto &Let = static_cast<const ExprLet &>(Root);
525 dfs(Let, Env);
526 break;
527 }
528 case Node::NK_ExprWith: {
529 const auto &With = static_cast<const ExprWith &>(Root);
530 dfs(With, Env);
531 break;
532 }
533 case Node::NK_ExprSelect: {
534 trivialDispatch(Root, Env);
535 const auto &Sel = static_cast<const ExprSelect &>(Root);
536 checkBuiltins(Sel);
537 break;
538 }
539 default:
540 trivialDispatch(Root, Env);
541 }
542}
543
545 // Create a basic env
546 DefBuilder DB(Diags);
547
548 for (const auto &[Name, Info] : PrimOpsInfo) {
549 if (!Info.Internal) {
550 // Only add non-internal primops without "__" prefix.
551 DB.addBuiltin(Name);
552 }
553 }
554
555 for (const auto &Builtin : Constants)
556 DB.addBuiltin(Builtin);
557
558 DB.addBuiltin("builtins");
559 // This is an undocumented keyword actually.
560 DB.addBuiltin(std::string("__curPos"));
561
562 auto Env = std::make_shared<EnvNode>(nullptr, DB.finish(), nullptr);
563
564 dfs(Root, Env);
565}
566
568 : Diags(Diags) {}
569
571 if (!Envs.contains(N))
572 return nullptr;
573 return Envs.at(N).get();
574}
Lookup variable names, from it's parent scope.
const std::string & staticName() const
Definition Attrs.h:50
bool isStatic() const
Definition Attrs.h:40
const std::vector< std::shared_ptr< AttrName > > & names() const
Definition Attrs.h:100
Node & key() const
Definition Attrs.h:217
Expr * value() const
Definition Attrs.h:219
AttributeKind kind() const
Definition Attrs.h:221
@ InheritFrom
inherit (expr) a b c
Definition Attrs.h:202
@ Inherit
inherit a b c;
Definition Attrs.h:200
DefinitionSource
"Source" information so we can know where the def comes from.
@ DS_Rec
From recursive attribute set. e.g. rec { }.
@ DS_LambdaArg
From ambda arg e.g. a: a + 1.
@ DS_LambdaNoArg_Formal
From lambda (noarg) formal, e.g. { a }: a + 1.
@ DS_Builtin
Builtin names.
@ DS_LambdaWithArg_Arg
From lambda (with @arg) arg, e.g. a in { foo }@a: foo + 1.
@ DS_With
From with <expr>;.
@ DS_LambdaWithArg_Formal
From lambda (with @arg) formal, e.g. foo in { foo }@a: foo + 1.
@ DS_Let
From let ... in ...
Fix & fix(std::string Message)
Definition Diagnostic.h:203
A set of variable definitions, which may inherit parent environment.
bool isLive() const
std::map< std::string, std::shared_ptr< Definition > > DefMap
bool isRecursive() const
Definition Attrs.h:287
const SemaAttrs & sema() const
Definition Attrs.h:289
const Misc * rec() const
Definition Attrs.h:285
Expr * body() const
Definition Lambda.h:119
LambdaArg * arg() const
Definition Lambda.h:118
const ExprAttrs * attrs() const
Definition Expr.h:155
const Expr * expr() const
Definition Expr.h:156
const Expr * desugaredFrom() const
Definition Expr.h:29
Expr & expr() const
Definition Expr.h:24
AttrPath * path() const
Definition Expr.h:35
Dot * dot() const
Definition Expr.h:31
const Identifier & id() const
Definition Simple.h:200
const Misc & kwWith() const
Definition Expr.h:178
Expr * with() const
Definition Expr.h:180
const Misc * tokSemi() const
Definition Expr.h:179
Expr * expr() const
Definition Expr.h:181
Fix & edit(TextEdit Edit)
Definition Diagnostic.h:65
const FormalVector & members() const
Definition Lambda.h:71
const std::map< std::string, const Formal * > & dedup()
Deduplicated formals.
Definition Lambda.h:74
const std::string & name() const
Definition Basic.h:120
Formals * formals() const
Definition Lambda.h:101
Identifier * id() const
Definition Lambda.h:99
LexerCursor lCur() const
Definition Range.h:116
LexerCursor rCur() const
Definition Range.h:117
NodeKind kind() const
Definition Basic.h:34
LexerCursorRange range() const
Definition Basic.h:35
virtual ChildVector children() const =0
void tag(DiagnosticTag Tag)
Definition Diagnostic.h:96
LexerCursorRange range() const
Definition Diagnostic.h:100
Attribute set after deduplication.
Definition Attrs.h:236
bool isRecursive() const
If the attribute set is rec.
Definition Attrs.h:269
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
static TextEdit mkRemoval(LexerCursorRange RemovingRange)
Definition Diagnostic.h:39
static TextEdit mkInsertion(LexerCursor P, std::string NewText)
Definition Diagnostic.h:35
const EnvNode * env(const Node *N) const
void runOnAST(const Node &Root)
Perform variable lookup analysis (def-use) on AST.
VariableLookupAnalysis(std::vector< Diagnostic > &Diags)
PrimopLookupResult lookupGlobalPrimOpInfo(const std::string &Name)
Look up information about a global primop by name.
std::map< std::string, nixf::PrimOpInfo > PrimOpsInfo
@ PrefixedFound
The primop was found, but needs "builtin." prefix.
Definition PrimOpInfo.h:44
@ NotFound
The primop was not found.
Definition PrimOpInfo.h:46
@ Found
The primop was found with an exact match.
Definition PrimOpInfo.h:42