Skip to content
This repository was archived by the owner on Jan 25, 2024. It is now read-only.

Commit e6a41cb

Browse files
authored
Merge pull request #88 from symphorien/unbound-ident
error diagnostics for unbound identifiers
2 parents 232a49e + 2a83372 commit e6a41cb

File tree

7 files changed

+254
-25
lines changed

7 files changed

+254
-25
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ nixpkgs-fmt-rnix = "1.2.0"
2727

2828
[dev-dependencies]
2929
stoppable_thread = "0.2.1"
30+
maplit = "1"
3031

3132
[features]
3233

src/error.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
use std::error::Error;
2+
13
use gc::{Finalize, Trace};
4+
use rnix::TextRange;
25

36
pub const ERR_PARSING: EvalError = EvalError::Internal(InternalError::Parsing);
47

8+
/// Used for rnix-lsp main functions
9+
#[derive(Debug, Clone, Trace, Finalize)]
10+
pub enum AppError {
11+
/// Unspecified internal error
12+
Internal(String),
13+
}
14+
15+
impl std::error::Error for AppError {}
16+
17+
impl std::fmt::Display for AppError {
18+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
19+
match self {
20+
AppError::Internal(x) => write!(f, "internal error: {}", x),
21+
}
22+
}
23+
}
524

25+
/// Could not evaluate an AST node
626
#[derive(Debug, Clone, Trace, Finalize)]
727
pub enum EvalError {
28+
/// failed to evaluate because of a limitation of our evaluator
829
Internal(InternalError),
30+
/// the code of the user definitely does not evaluate
931
Value(ValueError),
1032
}
1133

@@ -31,19 +53,45 @@ pub enum InternalError {
3153
Parsing,
3254
}
3355

34-
#[derive(Debug, Clone, Trace, Finalize)]
56+
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
3557
/// Used when we're confident that the user/code is at fault, such as
3658
/// division by zero. We use the parser directly for error reporting,
3759
/// so the evaluator returns its copies of parsing errors as internal,
3860
/// silent errors (see InternalError above).
39-
// TODO: store error text ranges to help the user figure out
40-
// exactly where an error is caused
4161
pub enum ValueError {
62+
/// Division by zero
4263
DivisionByZero,
64+
/// Type error
4365
TypeError(String),
66+
/// An attribute name is present twice in the same attrset
4467
AttrAlreadyDefined(String),
68+
/// An identifier is definitely not defined. Argument is the identifer.
69+
///
70+
/// Note that this should not be used when one cannot know like
71+
/// `with import <nixpkgs> {}; some_attr`
72+
UnboundIdentifier(String),
73+
}
74+
75+
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
76+
/// An error augmented with a location
77+
pub struct Located<T: Error + Clone + Trace + Finalize> {
78+
/// Where the error is located
79+
#[unsafe_ignore_trace]
80+
pub range: TextRange,
81+
/// The nature of the issue
82+
pub kind: T
4583
}
4684

85+
impl<T: Error + Clone + Trace + Finalize> std::error::Error for Located<T> {}
86+
87+
impl<T: Error + Clone + Trace + Finalize> std::fmt::Display for Located<T> {
88+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
89+
write!(f, "{} at {:?}", &self.kind, &self.range)
90+
}
91+
}
92+
93+
impl std::error::Error for ValueError {}
94+
4795
impl std::error::Error for EvalError {}
4896

4997
impl std::fmt::Display for EvalError {
@@ -71,6 +119,7 @@ impl std::fmt::Display for ValueError {
71119
ValueError::DivisionByZero => write!(f, "division by zero"),
72120
ValueError::AttrAlreadyDefined(name) => write!(f, "attribute `{}` defined more than once", name),
73121
ValueError::TypeError(msg) => write!(f, "{}", msg),
122+
ValueError::UnboundIdentifier(name) => write!(f, "identifier {} is unbound", name),
74123
}
75124
}
76125
}

src/main.rs

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod error;
2525
mod eval;
2626
mod lookup;
2727
mod parse;
28+
mod static_analysis;
2829
mod scope;
2930
mod tests;
3031
mod utils;
@@ -58,6 +59,8 @@ use std::{
5859
str::FromStr,
5960
};
6061

62+
use crate::error::AppError;
63+
6164
type Error = Box<dyn std::error::Error>;
6265

6366
fn main() {
@@ -250,21 +253,26 @@ impl App {
250253
}
251254
}
252255

256+
/// Common code of handle_notification between DidOpenTextDocument and DidChangeTextDocument
257+
fn handle_content(&mut self, uri: Url, content: String) -> Result<(), Error> {
258+
let parsed = rnix::parse(&content);
259+
let path = PathBuf::from_str(uri.path());
260+
let path = path.unwrap_or_else(|_| PathBuf::from("<unnamed>"));
261+
let gc_root = Gc::new(Scope::Root(path));
262+
let parsed_root = parsed.root().inner().ok_or(ERR_PARSING);
263+
let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root));
264+
self.files.insert(uri.clone(), (parsed, content, evaluated));
265+
self.send_diagnostics(uri)?;
266+
Ok(())
267+
}
268+
253269
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didChange
254270
fn handle_notification(&mut self, req: Notification) -> Result<(), Error> {
255271
match &*req.method {
256272
DidOpenTextDocument::METHOD => {
257273
let params: DidOpenTextDocumentParams = serde_json::from_value(req.params)?;
258274
let text = params.text_document.text;
259-
let parsed = rnix::parse(&text);
260-
self.send_diagnostics(params.text_document.uri.clone(), &text, &parsed)?;
261-
if let Ok(path) = PathBuf::from_str(params.text_document.uri.path()) {
262-
let gc_root = Gc::new(Scope::Root(path));
263-
let parsed_root = parsed.root().inner().ok_or(ERR_PARSING);
264-
let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root));
265-
self.files
266-
.insert(params.text_document.uri, (parsed, text, evaluated));
267-
}
275+
self.handle_content(params.text_document.uri, text)?;
268276
}
269277
DidChangeTextDocument::METHOD => {
270278
// Per the language server spec (https://git.io/JcrvY), we should apply changes
@@ -319,15 +327,7 @@ impl App {
319327

320328
content = new_content;
321329
}
322-
let parsed = rnix::parse(&content);
323-
self.send_diagnostics(uri.clone(), &content, &parsed)?;
324-
if let Ok(path) = PathBuf::from_str(uri.path()) {
325-
let gc_root = Gc::new(Scope::Root(path));
326-
let parsed_root = parsed.root().inner().ok_or(ERR_PARSING);
327-
let evaluated = parsed_root.and_then(|x| Expr::parse(x, gc_root));
328-
self.files
329-
.insert(uri, (parsed, content.to_owned().to_string(), evaluated));
330-
}
330+
self.handle_content(uri, content)?;
331331
}
332332
_ => (),
333333
}
@@ -580,7 +580,14 @@ impl App {
580580

581581
Some(lsp_links)
582582
}
583-
fn send_diagnostics(&mut self, uri: Url, code: &str, ast: &AST) -> Result<(), Error> {
583+
fn send_diagnostics(&mut self, uri: Url) -> Result<(), Error> {
584+
let (ast, code, parsed) = self.files.get(&uri).ok_or_else(|| {
585+
AppError::Internal(format!(
586+
"send_diagnostics called on unregistered uri {}",
587+
&uri
588+
))
589+
})?;
590+
// errors reported by rnix-parser
584591
let errors = ast.errors();
585592
let mut diagnostics = Vec::with_capacity(errors.len());
586593
for err in errors {
@@ -603,6 +610,26 @@ impl App {
603610
});
604611
}
605612
}
613+
// errors reported by crate::static_analysis
614+
let errors = match parsed {
615+
Ok(v) => static_analysis::check(v),
616+
Err(EvalError::Value(e)) => {
617+
let range = TextRange::up_to(TextSize::of(code));
618+
vec![error::Located { range, kind: e.clone() }]
619+
},
620+
Err(EvalError::Internal(_)) => {
621+
// don't report false positives
622+
vec![]
623+
},
624+
};
625+
for error in errors {
626+
diagnostics.push(Diagnostic {
627+
range: utils::range(code, error.range),
628+
severity: Some(DiagnosticSeverity::ERROR),
629+
message: error.kind.to_string(),
630+
..Diagnostic::default()
631+
});
632+
}
606633
self.notify(Notification::new(
607634
"textDocument/publishDiagnostics".into(),
608635
PublishDiagnosticsParams {

src/parse.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,11 @@ impl Expr {
307307
},
308308
}
309309
}
310-
ParsedType::Ident(ident) => ExprSource::Ident {
311-
name: ident.as_str().to_string(),
312-
},
310+
ParsedType::Ident(ident) => {
311+
ExprSource::Ident {
312+
name: ident.as_str().to_string(),
313+
}
314+
}
313315
ParsedType::Dynamic(dynamic) => ExprSource::Dynamic {
314316
inner: recurse_box(dynamic.inner().ok_or(ERR_PARSING)?),
315317
},

src/static_analysis.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::borrow::Borrow;
2+
3+
use rnix::TextRange;
4+
5+
use crate::{
6+
error::{EvalError, Located, ValueError},
7+
eval::{Expr, ExprSource},
8+
};
9+
10+
/// If this node is an identifier, should it be a variable name ?
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
enum Ident {
13+
/// If this node is an identifier, it should be a variable name
14+
IsVariable,
15+
/// If this not is an identifier, it is not a variable name
16+
///
17+
/// Example: foo in `bar.foo`
18+
IsNotVariable,
19+
}
20+
21+
/// If this is an error, maybe report it to acc. Otherwise, visit all children of this node
22+
/// to find evaluation errors and collect diagnostics on this accumulator
23+
fn visit_result<T: Borrow<Expr>>(
24+
acc: &mut Vec<Located<ValueError>>,
25+
result: &Result<T, EvalError>,
26+
range: &Option<TextRange>,
27+
ident_ctx: Ident,
28+
) {
29+
match result {
30+
Err(EvalError::Value(err)) => {
31+
if let Some(range) = range {
32+
acc.push(Located {
33+
range: range.clone(),
34+
kind: err.clone(),
35+
})
36+
}
37+
}
38+
Err(EvalError::Internal(_)) => {}
39+
Ok(v) => visit(acc, v.borrow(), ident_ctx),
40+
}
41+
}
42+
43+
/// Visit all children of this node to find evaluation errors and collect
44+
/// diagnostics on this accumulator
45+
fn visit(acc: &mut Vec<Located<ValueError>>, node: &Expr, ident_ctx: Ident) {
46+
use Ident::*;
47+
match &node.source {
48+
ExprSource::AttrSet { definitions } => {
49+
for i in definitions.iter() {
50+
visit_result(acc, i, &node.range, IsVariable)
51+
}
52+
}
53+
ExprSource::KeyValuePair { key, value } => {
54+
visit_result(acc, key, &node.range, IsNotVariable);
55+
visit_result(acc, value, &node.range, IsVariable);
56+
}
57+
ExprSource::Select { from, index } => {
58+
visit_result(acc, from, &node.range, IsVariable);
59+
visit_result(acc, index, &node.range, IsNotVariable);
60+
}
61+
ExprSource::Ident { name } => {
62+
if ident_ctx == IsVariable {
63+
if let Some(range) = &node.range {
64+
// check that the variable is bound
65+
if node.scope.get_let(name).is_none() {
66+
acc.push(Located {
67+
range: range.clone(),
68+
kind: ValueError::UnboundIdentifier(name.clone()),
69+
});
70+
}
71+
}
72+
}
73+
}
74+
ExprSource::Literal { .. } => {}
75+
ExprSource::UnaryInvert { value: inner }
76+
| ExprSource::UnaryNegate { value: inner }
77+
| ExprSource::Dynamic { inner }
78+
| ExprSource::Paren { inner } => visit_result(acc, inner, &node.range, IsVariable),
79+
ExprSource::BinOp { left, right, .. }
80+
| ExprSource::BoolAnd { left, right }
81+
| ExprSource::Implication { left, right }
82+
| ExprSource::BoolOr { left, right } => {
83+
visit_result(acc, left, &node.range, IsVariable);
84+
visit_result(acc, right, &node.range, IsVariable);
85+
}
86+
}
87+
}
88+
/// Looks for errors in the user code. Attempts to return no false positives.
89+
///
90+
/// Current analysis can detect undefined variables.
91+
pub fn check(node: &Expr) -> Vec<Located<ValueError>> {
92+
let mut res = Vec::new();
93+
visit(&mut res, node, Ident::IsVariable);
94+
res
95+
}

0 commit comments

Comments
 (0)