From 963fa5f7e2cd0d25f79a9a3fbad8b6ceaa4ac82b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 16 Dec 2025 11:50:26 -0500 Subject: [PATCH 1/3] Catch loops with a `MISSING` `body` --- crates/ark/src/lsp/diagnostics_syntax.rs | 98 +++++++++++++++++++ ..._syntax__tests__for_loop_with_no_body.snap | 5 + ...ntax__tests__repeat_loop_with_no_body.snap | 5 + ...yntax__tests__while_loop_with_no_body.snap | 5 + 4 files changed, 113 insertions(+) create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__for_loop_with_no_body.snap create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__repeat_loop_with_no_body.snap create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__while_loop_with_no_body.snap diff --git a/crates/ark/src/lsp/diagnostics_syntax.rs b/crates/ark/src/lsp/diagnostics_syntax.rs index 277aff831..991a3b4c6 100644 --- a/crates/ark/src/lsp/diagnostics_syntax.rs +++ b/crates/ark/src/lsp/diagnostics_syntax.rs @@ -134,6 +134,9 @@ fn diagnose_missing( NodeType::Subset => diagnose_missing_subset(node, context, diagnostics), NodeType::Subset2 => diagnose_missing_subset2(node, context, diagnostics), NodeType::BinaryOperator(_) => diagnose_missing_binary_operator(node, context, diagnostics), + NodeType::ForStatement => diagnose_missing_for(node, context, diagnostics), + NodeType::WhileStatement => diagnose_missing_while(node, context, diagnostics), + NodeType::RepeatStatement => diagnose_missing_repeat(node, context, diagnostics), _ => Ok(()), } } @@ -199,6 +202,59 @@ fn diagnose_missing_call_like( diagnose_missing_close(arguments, context, diagnostics, close_token) } +fn diagnose_missing_for( + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + diagnose_missing_loop_body("for", node, context, diagnostics) +} + +fn diagnose_missing_while( + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + diagnose_missing_loop_body("while", node, context, diagnostics) +} + +fn diagnose_missing_repeat( + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + diagnose_missing_loop_body("repeat", node, context, diagnostics) +} + +fn diagnose_missing_loop_body( + name: &str, + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + let Some(body) = node.child_by_field_name("body") else { + return Ok(()); + }; + + if !body.is_missing() { + // Everything is normal + return Ok(()); + } + + // Highlight just the loop's token name (like `while` or `for` or `repeat`). + // Don't want to highlight whole `node` because that can confusingly span many lines. + // Unfortunately we don't have a field name for these. + let Some(token) = node.child(0) else { + return Ok(()); + }; + + let range = token.range(); + let message = format!("Invalid {name} loop. Missing a body."); + diagnostics.push(new_syntax_diagnostic(message, range, context)); + + Ok(()) +} + fn diagnose_missing_binary_operator( node: Node, context: &DiagnosticContext, @@ -507,6 +563,48 @@ function(x { assert_eq!(diagnostic.range.end, Position::new(3, 1)); } + #[test] + fn test_for_loop_with_no_body() { + let text = "for(i in 1:10)"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `for` + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(0, 0)); + assert_eq!(diagnostic.range.end, Position::new(0, 3)); + } + + #[test] + fn test_while_loop_with_no_body() { + let text = "while(1)"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `while` + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(0, 0)); + assert_eq!(diagnostic.range.end, Position::new(0, 5)); + } + + #[test] + fn test_repeat_loop_with_no_body() { + let text = "1\nrepeat"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `repeat` + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(1, 0)); + assert_eq!(diagnostic.range.end, Position::new(1, 6)); + } + #[test] fn test_repeated_call_arguments_without_delimiter() { let text = "match(1, 2 3)"; diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__for_loop_with_no_body.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__for_loop_with_no_body.snap new file mode 100644 index 000000000..17e1696c3 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__for_loop_with_no_body.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid for loop. Missing a body. diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__repeat_loop_with_no_body.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__repeat_loop_with_no_body.snap new file mode 100644 index 000000000..e957e49e2 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__repeat_loop_with_no_body.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid repeat loop. Missing a body. diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__while_loop_with_no_body.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__while_loop_with_no_body.snap new file mode 100644 index 000000000..b1eb107b8 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__while_loop_with_no_body.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid while loop. Missing a body. From 4b8722d2796f7adf0ab114e221a55b2a4f61e2db Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 16 Dec 2025 13:18:17 -0500 Subject: [PATCH 2/3] Show `MISSING` node issues for loop bodies and if statement consequences --- crates/ark/src/lsp/diagnostics_syntax.rs | 44 +++++++++++++++++++ ...sts__if_statement_with_no_consequence.snap | 5 +++ 2 files changed, 49 insertions(+) create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__if_statement_with_no_consequence.snap diff --git a/crates/ark/src/lsp/diagnostics_syntax.rs b/crates/ark/src/lsp/diagnostics_syntax.rs index 991a3b4c6..ad1b22f6f 100644 --- a/crates/ark/src/lsp/diagnostics_syntax.rs +++ b/crates/ark/src/lsp/diagnostics_syntax.rs @@ -137,6 +137,7 @@ fn diagnose_missing( NodeType::ForStatement => diagnose_missing_for(node, context, diagnostics), NodeType::WhileStatement => diagnose_missing_while(node, context, diagnostics), NodeType::RepeatStatement => diagnose_missing_repeat(node, context, diagnostics), + NodeType::IfStatement => diagnose_missing_if(node, context, diagnostics), _ => Ok(()), } } @@ -255,6 +256,34 @@ fn diagnose_missing_loop_body( Ok(()) } +fn diagnose_missing_if( + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + let Some(consequence) = node.child_by_field_name("consequence") else { + return Ok(()); + }; + + if !consequence.is_missing() { + // Everything is normal + return Ok(()); + } + + // Highlight just the token name (i.e. `if`). + // Don't want to highlight whole `node` because that can confusingly span many lines. + // Unfortunately we don't have a field name for this. + let Some(token) = node.child(0) else { + return Ok(()); + }; + + let range = token.range(); + let message = format!("Invalid if statement. Missing a body."); + diagnostics.push(new_syntax_diagnostic(message, range, context)); + + Ok(()) +} + fn diagnose_missing_binary_operator( node: Node, context: &DiagnosticContext, @@ -605,6 +634,21 @@ function(x { assert_eq!(diagnostic.range.end, Position::new(1, 6)); } + #[test] + fn test_if_statement_with_no_consequence() { + let text = "if (a)"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `if` + // We call if an if "body" even though tree-sitter calls it a "consequence" + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(0, 0)); + assert_eq!(diagnostic.range.end, Position::new(0, 2)); + } + #[test] fn test_repeated_call_arguments_without_delimiter() { let text = "match(1, 2 3)"; diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__if_statement_with_no_consequence.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__if_statement_with_no_consequence.snap new file mode 100644 index 000000000..e2e440422 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__if_statement_with_no_consequence.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid if statement. Missing a body. From 4d4997109afa0e1b14778c9ebf4343847343935f Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 7 Jan 2026 09:44:18 -0500 Subject: [PATCH 3/3] Also flag `function()` with no body --- crates/ark/src/lsp/diagnostics_syntax.rs | 56 +++++++++++++++++++ ...s__function_definition_with_no_body-2.snap | 5 ++ ...sts__function_definition_with_no_body.snap | 5 ++ 3 files changed, 66 insertions(+) create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body-2.snap create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body.snap diff --git a/crates/ark/src/lsp/diagnostics_syntax.rs b/crates/ark/src/lsp/diagnostics_syntax.rs index ad1b22f6f..33134d8b0 100644 --- a/crates/ark/src/lsp/diagnostics_syntax.rs +++ b/crates/ark/src/lsp/diagnostics_syntax.rs @@ -138,6 +138,9 @@ fn diagnose_missing( NodeType::WhileStatement => diagnose_missing_while(node, context, diagnostics), NodeType::RepeatStatement => diagnose_missing_repeat(node, context, diagnostics), NodeType::IfStatement => diagnose_missing_if(node, context, diagnostics), + NodeType::FunctionDefinition => { + diagnose_missing_function_definition(node, context, diagnostics) + }, _ => Ok(()), } } @@ -284,6 +287,33 @@ fn diagnose_missing_if( Ok(()) } +fn diagnose_missing_function_definition( + node: Node, + context: &DiagnosticContext, + diagnostics: &mut Vec, +) -> anyhow::Result<()> { + let Some(body) = node.child_by_field_name("body") else { + return Ok(()); + }; + + if !body.is_missing() { + // Everything is normal + return Ok(()); + } + + // Highlight just the token name (i.e. `function`). + // Don't want to highlight whole `node` because that can confusingly span many lines. + let Some(name) = node.child_by_field_name("name") else { + return Ok(()); + }; + + let range = name.range(); + let message = format!("Invalid function definition. Missing a body."); + diagnostics.push(new_syntax_diagnostic(message, range, context)); + + Ok(()) +} + fn diagnose_missing_binary_operator( node: Node, context: &DiagnosticContext, @@ -649,6 +679,32 @@ function(x { assert_eq!(diagnostic.range.end, Position::new(0, 2)); } + #[test] + fn test_function_definition_with_no_body() { + let text = "function(a)"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `function` + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(0, 0)); + assert_eq!(diagnostic.range.end, Position::new(0, 8)); + + // Anonymous function + let text = r"\(a)"; + + let diagnostics = text_diagnostics(text); + assert_eq!(diagnostics.len(), 1); + + // Diagnostic highlights the `function` + let diagnostic = diagnostics.get(0).unwrap(); + insta::assert_snapshot!(diagnostic.message); + assert_eq!(diagnostic.range.start, Position::new(0, 0)); + assert_eq!(diagnostic.range.end, Position::new(0, 1)); + } + #[test] fn test_repeated_call_arguments_without_delimiter() { let text = "match(1, 2 3)"; diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body-2.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body-2.snap new file mode 100644 index 000000000..9731cd727 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid function definition. Missing a body. diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body.snap b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body.snap new file mode 100644 index 000000000..9731cd727 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__diagnostics_syntax__tests__function_definition_with_no_body.snap @@ -0,0 +1,5 @@ +--- +source: crates/ark/src/lsp/diagnostics_syntax.rs +expression: diagnostic.message +--- +Invalid function definition. Missing a body.