nixd
Loading...
Searching...
No Matches
VariableLookup.cpp
Go to the documentation of this file.
6
7#include <set>
8
9using namespace nixf;
10
11namespace {
12
13std::set<std::string> Constants{
14 "true", "false", "null",
15 "__currentTime", "__currentSystem", "__nixVersion",
16 "__storeDir", "__langVersion", "__importNative",
17 "__traceVerbose", "__nixPath", "derivation",
18};
19
20/// Builder a map of definitions. If there are something overlapped, maybe issue
21/// a diagnostic.
22class DefBuilder {
24 std::vector<Diagnostic> &Diags;
25
26 std::shared_ptr<Definition> addSimple(std::string Name, const Node *Entry,
28 assert(!Def.contains(Name));
29 auto NewDef = std::make_shared<Definition>(Entry, Source);
30 Def.insert({std::move(Name), NewDef});
31 return NewDef;
32 }
33
34public:
35 DefBuilder(std::vector<Diagnostic> &Diags) : Diags(Diags) {}
36
37 void addBuiltin(std::string Name) {
38 // Don't need to record def map for builtins.
39 auto _ = addSimple(std::move(Name), nullptr, Definition::DS_Builtin);
40 }
41
42 [[nodiscard("Record ToDef Map!")]] std::shared_ptr<Definition>
43 add(std::string Name, const Node *Entry,
45 auto PrimOpLookup = lookupGlobalPrimOpInfo(Name);
46 if (PrimOpLookup != PrimopLookupResult::NotFound) {
47 // Overriding a builtin primop is discouraged.
48 Diagnostic &D =
49 Diags.emplace_back(Diagnostic::DK_PrimOpOverridden, Entry->range());
50 D << Name;
51 }
52
53 // Lookup constants
54 if (Constants.contains(Name)) {
55 Diagnostic &D =
56 Diags.emplace_back(Diagnostic::DK_ConstantOverridden, Entry->range());
57 D << Name;
58 }
59
60 return addSimple(std::move(Name), Entry, Source);
61 }
62
63 EnvNode::DefMap finish() { return std::move(Def); }
64};
65
66} // namespace
67
68bool EnvNode::isLive() const {
69 for (const auto &[_, D] : Defs) {
70 if (!D->uses().empty())
71 return true;
72 }
73 return false;
74}
75
76void VariableLookupAnalysis::emitEnvLivenessWarning(
77 const std::shared_ptr<EnvNode> &NewEnv) {
78 for (const auto &[Name, Def] : NewEnv->defs()) {
79 // If the definition comes from lambda arg, omit the diagnostic
80 // because there is no elegant way to "fix" this trivially & keep
81 // the lambda signature.
82 if (Def->source() == Definition::DS_LambdaArg)
83 continue;
84 // Ignore builtins usage.
85 if (!Def->syntax())
86 continue;
87 if (Def->uses().empty()) {
88 Diagnostic::DiagnosticKind Kind = [&]() {
89 switch (Def->source()) {
91 return Diagnostic::DK_UnusedDefLet;
93 return Diagnostic::DK_UnusedDefLambdaNoArg_Formal;
95 return Diagnostic::DK_UnusedDefLambdaWithArg_Formal;
97 return Diagnostic::DK_UnusedDefLambdaWithArg_Arg;
98 default:
99 assert(false && "liveness diagnostic encountered an unknown source!");
100 __builtin_unreachable();
101 }
102 }();
103 Diagnostic &D = Diags.emplace_back(Kind, Def->syntax()->range());
104 D << Name;
106 }
107 }
108}
109
110void VariableLookupAnalysis::lookupVar(const ExprVar &Var,
111 const std::shared_ptr<EnvNode> &Env) {
112 const auto &Name = Var.id().name();
113 const auto *CurEnv = Env.get();
114 std::shared_ptr<Definition> Def;
115 std::vector<const EnvNode *> WithEnvs;
116 for (; CurEnv; CurEnv = CurEnv->parent()) {
117 if (CurEnv->defs().contains(Name)) {
118 Def = CurEnv->defs().at(Name);
119 break;
120 }
121 // Find all nested "with" expression, variables potentially come from those.
122 // For example
123 // with lib;
124 // with builtins;
125 // generators <--- this variable may come from "lib" | "builtins"
126 //
127 // We cannot determine where it precisely come from, thus mark all Envs
128 // alive.
129 if (CurEnv->isWith()) {
130 WithEnvs.emplace_back(CurEnv);
131 }
132 }
133
134 if (Def) {
135 Def->usedBy(Var);
136 Results.insert({&Var, LookupResult{LookupResultKind::Defined, Def}});
137 } else if (!WithEnvs.empty()) { // comes from enclosed "with" expressions.
138 for (const auto *WithEnv : WithEnvs) {
139 Def = WithDefs.at(WithEnv->syntax());
140 Def->usedBy(Var);
141 }
142 Results.insert({&Var, LookupResult{LookupResultKind::FromWith, Def}});
143 } else {
144 // Check if this is a primop.
145 switch (lookupGlobalPrimOpInfo(Name)) {
147 assert(false && "primop name should be defined");
148 break;
150 Diagnostic &D =
151 Diags.emplace_back(Diagnostic::DK_PrimOpNeedsPrefix, Var.range());
152 D.fix("use `builtins.` prefix")
153 .edit(TextEdit::mkInsertion(Var.range().lCur(), "builtins."));
154 Results.insert(
155 {&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
156 break;
157 }
159 // Otherwise, this variable is undefined.
160 Results.insert(
161 {&Var, LookupResult{LookupResultKind::Undefined, nullptr}});
162 Diagnostic &Diag =
163 Diags.emplace_back(Diagnostic::DK_UndefinedVariable, Var.range());
164 Diag << Var.id().name();
165 break;
166 }
167 }
168}
169
170void VariableLookupAnalysis::dfs(const ExprLambda &Lambda,
171 const std::shared_ptr<EnvNode> &Env) {
172 // Early exit for in-complete lambda.
173 if (!Lambda.body())
174 return;
175
176 // Create a new EnvNode, as lambdas may have formal & arg.
177 DefBuilder DBuilder(Diags);
178 assert(Lambda.arg());
179 const LambdaArg &Arg = *Lambda.arg();
180
181 // foo: body
182 // ^~~<------- add function argument.
183 if (Arg.id()) {
184 if (!Arg.formals()) {
185 ToDef.insert_or_assign(Arg.id(), DBuilder.add(Arg.id()->name(), Arg.id(),
187 // Function arg cannot duplicate to it's formal.
188 // If it this unluckily happens, we would like to skip this definition.
189 } else if (!Arg.formals()->dedup().contains(Arg.id()->name())) {
190 ToDef.insert_or_assign(Arg.id(),
191 DBuilder.add(Arg.id()->name(), Arg.id(),
193 }
194 }
195
196 // { foo, bar, ... } : body
197 // ^~~~~~~~~<-------------- add function formals.
198
199 // This section differentiates between formal parameters with an argument and
200 // without. Example:
201 //
202 // { foo }@arg : use arg
203 //
204 // In this case, the definition of `foo` is not used directly; however, it
205 // might be accessed via arg.foo. Therefore, the severity of an unused formal
206 // parameter is reduced in this scenario.
207 if (Arg.formals()) {
208 for (const auto &[Name, Formal] : Arg.formals()->dedup()) {
212 ToDef.insert_or_assign(Formal->id(),
213 DBuilder.add(Name, Formal->id(), Source));
214 }
215 }
216
217 auto NewEnv = std::make_shared<EnvNode>(Env, DBuilder.finish(), &Lambda);
218
219 if (Arg.formals()) {
220 for (const auto &Formal : Arg.formals()->members()) {
221 if (const Expr *Def = Formal->defaultExpr()) {
222 dfs(*Def, NewEnv);
223 }
224 }
225 }
226
227 dfs(*Lambda.body(), NewEnv);
228
229 emitEnvLivenessWarning(NewEnv);
230}
231
232void VariableLookupAnalysis::dfsDynamicAttrs(
233 const std::vector<Attribute> &DynamicAttrs,
234 const std::shared_ptr<EnvNode> &Env) {
235 for (const auto &Attr : DynamicAttrs) {
236 if (!Attr.value())
237 continue;
238 dfs(Attr.key(), Env);
239 dfs(*Attr.value(), Env);
240 }
241}
242
243std::shared_ptr<EnvNode> VariableLookupAnalysis::dfsAttrs(
244 const SemaAttrs &SA, const std::shared_ptr<EnvNode> &Env,
245 const Node *Syntax, Definition::DefinitionSource Source) {
246 if (SA.isRecursive()) {
247 // rec { }, or let ... in ...
248 DefBuilder DB(Diags);
249 // For each static names, create a name binding.
250 for (const auto &[Name, Attr] : SA.staticAttrs())
251 ToDef.insert_or_assign(&Attr.key(), DB.add(Name, &Attr.key(), Source));
252
253 auto NewEnv = std::make_shared<EnvNode>(Env, DB.finish(), Syntax);
254
255 for (const auto &[_, Attr] : SA.staticAttrs()) {
256 if (!Attr.value())
257 continue;
258 if (Attr.kind() == Attribute::AttributeKind::Plain ||
260 dfs(*Attr.value(), NewEnv);
261 } else {
262 assert(Attr.kind() == Attribute::AttributeKind::Inherit);
263 dfs(*Attr.value(), Env);
264 }
265 }
266
267 dfsDynamicAttrs(SA.dynamicAttrs(), NewEnv);
268 return NewEnv;
269 }
270
271 // Non-recursive. Dispatch nested node with old Env
272 for (const auto &[_, Attr] : SA.staticAttrs()) {
273 if (!Attr.value())
274 continue;
275 dfs(*Attr.value(), Env);
276 }
277
278 dfsDynamicAttrs(SA.dynamicAttrs(), Env);
279 return Env;
280};
281
282void VariableLookupAnalysis::dfs(const ExprAttrs &Attrs,
283 const std::shared_ptr<EnvNode> &Env) {
284 const SemaAttrs &SA = Attrs.sema();
285 std::shared_ptr<EnvNode> NewEnv =
286 dfsAttrs(SA, Env, &Attrs, Definition::DS_Rec);
287 if (NewEnv != Env) {
288 assert(Attrs.isRecursive() &&
289 "NewEnv must be created for recursive attrset");
290 if (!NewEnv->isLive()) {
291 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_ExtraRecursive,
292 Attrs.rec()->range());
293 D.fix("remove `rec` keyword")
294 .edit(TextEdit::mkRemoval(Attrs.rec()->range()));
296 }
297 }
298}
299
300void VariableLookupAnalysis::dfs(const ExprLet &Let,
301 const std::shared_ptr<EnvNode> &Env) {
302
303 // Obtain the env object suitable for "in" expression.
304 auto GetLetEnv = [&Env, &Let, this]() -> std::shared_ptr<EnvNode> {
305 // This is an empty let ... in ... expr, definitely anti-pattern in
306 // nix language. We want to passthrough the env then.
307 if (!Let.attrs()) {
308 return Env;
309 }
310
311 // If there are some attributes actually, create a new env.
312 const SemaAttrs &SA = Let.attrs()->sema();
313 assert(SA.isRecursive() && "let ... in ... attrset must be recursive");
314 return dfsAttrs(SA, Env, &Let, Definition::DS_Let);
315 };
316
317 auto LetEnv = GetLetEnv();
318
319 if (Let.expr())
320 dfs(*Let.expr(), LetEnv);
321 emitEnvLivenessWarning(LetEnv);
322}
323
324void VariableLookupAnalysis::trivialDispatch(
325 const Node &Root, const std::shared_ptr<EnvNode> &Env) {
326 for (const Node *Ch : Root.children()) {
327 if (!Ch)
328 continue;
329 dfs(*Ch, Env);
330 }
331}
332
333void VariableLookupAnalysis::dfs(const ExprWith &With,
334 const std::shared_ptr<EnvNode> &Env) {
335 auto NewEnv = std::make_shared<EnvNode>(Env, EnvNode::DefMap{}, &With);
336 if (!WithDefs.contains(&With)) {
337 auto NewDef =
338 std::make_shared<Definition>(&With.kwWith(), Definition::DS_With);
339 ToDef.insert_or_assign(&With.kwWith(), NewDef);
340 WithDefs.insert_or_assign(&With, NewDef);
341 }
342
343 if (With.with())
344 dfs(*With.with(), Env);
345
346 if (With.expr())
347 dfs(*With.expr(), NewEnv);
348
349 if (WithDefs.at(&With)->uses().empty()) {
350 Diagnostic &D =
351 Diags.emplace_back(Diagnostic::DK_ExtraWith, With.kwWith().range());
352 Fix &F = D.fix("remove `with` expression")
354 if (With.tokSemi())
356 if (With.with())
357 F.edit(TextEdit::mkRemoval(With.with()->range()));
358 }
359}
360
361bool isBuiltinConstant(const std::string &Name) {
362 if (Name.starts_with("_"))
363 return false;
364 return Constants.contains(Name) || Constants.contains("__" + Name);
365}
366
367void VariableLookupAnalysis::checkBuiltins(const ExprSelect &Sel) {
368 if (!Sel.path())
369 return;
370
371 if (Sel.expr().kind() != Node::NK_ExprVar)
372 return;
373
374 const auto &Builtins = static_cast<const ExprVar &>(Sel.expr());
375 if (Builtins.id().name() != "builtins")
376 return;
377
378 const auto &AP = *Sel.path();
379
380 if (AP.names().size() != 1)
381 return;
382
383 AttrName &First = *AP.names()[0];
384 if (!First.isStatic())
385 return;
386
387 const auto &Name = First.staticName();
388
389 switch (lookupGlobalPrimOpInfo(Name)) {
391 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_PrimOpRemovablePrefix,
392 Builtins.range());
393 Fix &F =
394 D.fix("remove `builtins.` prefix")
395 .edit(TextEdit::mkRemoval(Builtins.range())); // remove `builtins`
396
397 if (Sel.dot()) {
398 // remove the dot also.
399 F.edit(TextEdit::mkRemoval(Sel.dot()->range()));
400 }
401 return;
402 }
404 return;
406 if (!isBuiltinConstant(Name)) {
407 Diagnostic &D = Diags.emplace_back(Diagnostic::DK_PrimOpUnknown,
408 AP.names()[0]->range());
409 D << Name;
410 return;
411 }
412 }
413}
414
415void VariableLookupAnalysis::dfs(const Node &Root,
416 const std::shared_ptr<EnvNode> &Env) {
417 Envs.insert({&Root, Env});
418 switch (Root.kind()) {
419 case Node::NK_ExprVar: {
420 const auto &Var = static_cast<const ExprVar &>(Root);
421 lookupVar(Var, Env);
422 break;
423 }
424 case Node::NK_ExprLambda: {
425 const auto &Lambda = static_cast<const ExprLambda &>(Root);
426 dfs(Lambda, Env);
427 break;
428 }
429 case Node::NK_ExprAttrs: {
430 const auto &Attrs = static_cast<const ExprAttrs &>(Root);
431 dfs(Attrs, Env);
432 break;
433 }
434 case Node::NK_ExprLet: {
435 const auto &Let = static_cast<const ExprLet &>(Root);
436 dfs(Let, Env);
437 break;
438 }
439 case Node::NK_ExprWith: {
440 const auto &With = static_cast<const ExprWith &>(Root);
441 dfs(With, Env);
442 break;
443 }
444 case Node::NK_ExprSelect: {
445 trivialDispatch(Root, Env);
446 const auto &Sel = static_cast<const ExprSelect &>(Root);
447 checkBuiltins(Sel);
448 break;
449 }
450 default:
451 trivialDispatch(Root, Env);
452 }
453}
454
456 // Create a basic env
457 DefBuilder DB(Diags);
458
459 for (const auto &[Name, Info] : PrimOpsInfo) {
460 if (!Info.Internal) {
461 // Only add non-internal primops without "__" prefix.
462 DB.addBuiltin(Name);
463 }
464 }
465
466 for (const auto &Builtin : Constants)
467 DB.addBuiltin(Builtin);
468
469 DB.addBuiltin("builtins");
470 // This is an undocumented keyword actually.
471 DB.addBuiltin(std::string("__curPos"));
472
473 auto Env = std::make_shared<EnvNode>(nullptr, DB.finish(), nullptr);
474
475 dfs(Root, Env);
476}
477
479 : Diags(Diags) {}
480
482 if (!Envs.contains(N))
483 return nullptr;
484 return Envs.at(N).get();
485}
bool isBuiltinConstant(const std::string &Name)
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
@ 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:151
const Expr * expr() const
Definition Expr.h:152
Expr & expr() const
Definition Expr.h:22
AttrPath * path() const
Definition Expr.h:31
Dot * dot() const
Definition Expr.h:27
const Identifier & id() const
Definition Simple.h:200
const Misc & kwWith() const
Definition Expr.h:174
Expr * with() const
Definition Expr.h:176
const Misc * tokSemi() const
Definition Expr.h:175
Expr * expr() const
Definition Expr.h:177
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
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
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