Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions helix-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dotenvy = "0.15.7"
tokio-tungstenite = "0.27.0"
futures-util = "0.3.31"
regex = "1.11.2"
open = "5.3"

[dev-dependencies]
tempfile = "3.14.0"
Expand Down
6 changes: 3 additions & 3 deletions helix-cli/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub async fn run(instance_name: String, metrics_sender: &MetricsSender) -> Resul
Ok(metrics_data.clone())
}

async fn ensure_helix_repo_cached() -> Result<()> {
pub(crate) async fn ensure_helix_repo_cached() -> Result<()> {
let repo_cache = get_helix_repo_cache()?;

if needs_cache_recreation(&repo_cache)? {
Expand Down Expand Up @@ -223,7 +223,7 @@ fn update_git_cache(repo_cache: &std::path::Path) -> Result<()> {
Ok(())
}

async fn prepare_instance_workspace(project: &ProjectContext, instance_name: &str) -> Result<()> {
pub(crate) async fn prepare_instance_workspace(project: &ProjectContext, instance_name: &str) -> Result<()> {
print_status(
"PREPARE",
&format!("Preparing workspace for '{instance_name}'"),
Expand Down Expand Up @@ -253,7 +253,7 @@ async fn prepare_instance_workspace(project: &ProjectContext, instance_name: &st
Ok(())
}

async fn compile_project(project: &ProjectContext, instance_name: &str) -> Result<MetricsData> {
pub(crate) async fn compile_project(project: &ProjectContext, instance_name: &str) -> Result<MetricsData> {
print_status("COMPILE", "Compiling Helix queries...");

// Create helix-container directory in instance workspace for generated files
Expand Down
184 changes: 164 additions & 20 deletions helix-cli/src/commands/check.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,139 @@
//! Check command - validates project configuration, queries, and generated Rust code.

use crate::commands::build;
use crate::github_issue::{GitHubIssueBuilder, filter_errors_only};
use crate::metrics_sender::MetricsSender;
use crate::project::ProjectContext;
use crate::utils::helixc_utils::{
analyze_source, collect_hx_files, generate_content, parse_content,
analyze_source, collect_hx_contents, collect_hx_files, generate_content, parse_content,
};
use crate::utils::{print_status, print_success};
use crate::utils::{print_confirm, print_error, print_status, print_success, print_warning};
use eyre::Result;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;

/// Output from running cargo check.
struct CargoCheckOutput {
success: bool,
#[allow(dead_code)] // May be useful for debugging
full_output: String,
errors_only: String,
}

pub async fn run(instance: Option<String>) -> Result<()> {
pub async fn run(instance: Option<String>, metrics_sender: &MetricsSender) -> Result<()> {
// Load project context
let project = ProjectContext::find_and_load(None)?;

match instance {
Some(instance_name) => check_instance(&project, &instance_name).await,
None => check_all_instances(&project).await,
Some(instance_name) => check_instance(&project, &instance_name, metrics_sender).await,
None => check_all_instances(&project, metrics_sender).await,
}
}

async fn check_instance(project: &ProjectContext, instance_name: &str) -> Result<()> {
async fn check_instance(
project: &ProjectContext,
instance_name: &str,
metrics_sender: &MetricsSender,
) -> Result<()> {
let start_time = Instant::now();

print_status("CHECK", &format!("Checking instance '{instance_name}'"));

// Validate instance exists in config
let _instance_config = project.config.get_instance(instance_name)?;

// Validate queries and schema syntax
// Step 1: Validate syntax first (quick check)
print_status("SYNTAX", "Validating query syntax...");
validate_project_syntax(project)?;
print_success("Syntax validation passed");

// Step 2: Ensure helix repo is cached (reuse from build.rs)
build::ensure_helix_repo_cached().await?;

// Step 3: Prepare instance workspace (reuse from build.rs)
build::prepare_instance_workspace(project, instance_name).await?;

// Step 4: Compile project - generate queries.rs (reuse from build.rs)
let metrics_data = build::compile_project(project, instance_name).await?;

// Step 5: Copy generated files to helix-repo-copy for cargo check
let instance_workspace = project.instance_workspace(instance_name);
let generated_src = instance_workspace.join("helix-container/src");
let cargo_check_src = instance_workspace.join("helix-repo-copy/helix-container/src");

// Copy queries.rs and config.hx.json
fs::copy(
generated_src.join("queries.rs"),
cargo_check_src.join("queries.rs"),
)?;
fs::copy(
generated_src.join("config.hx.json"),
cargo_check_src.join("config.hx.json"),
)?;

// Step 6: Run cargo check
print_status("CARGO", "Running cargo check on generated code...");
let helix_container_dir = instance_workspace.join("helix-repo-copy/helix-container");
let cargo_output = run_cargo_check(&helix_container_dir)?;

let compile_time = start_time.elapsed().as_secs() as u32;

if !cargo_output.success {
// Send failure telemetry
metrics_sender.send_compile_event(
instance_name.to_string(),
metrics_data.queries_string,
metrics_data.num_of_queries,
compile_time,
false,
Some(cargo_output.errors_only.clone()),
);

// Read generated Rust for issue
let generated_rust = fs::read_to_string(cargo_check_src.join("queries.rs"))
.unwrap_or_else(|_| String::from("[Could not read generated code]"));

// Handle failure - print errors and offer GitHub issue
handle_cargo_check_failure(&cargo_output, &generated_rust, project)?;

return Err(eyre::eyre!("Cargo check failed on generated Rust code"));
}

print_success("Cargo check passed");
print_success(&format!(
"Instance '{instance_name}' configuration is valid"
"Instance '{}' check completed successfully",
instance_name
));
Ok(())
}

async fn check_all_instances(project: &ProjectContext) -> Result<()> {
async fn check_all_instances(
project: &ProjectContext,
metrics_sender: &MetricsSender,
) -> Result<()> {
print_status("CHECK", "Checking all instances");

// Validate queries and schema syntax
validate_project_syntax(project)?;
let instances: Vec<String> = project.config.list_instances().into_iter().map(String::from).collect();

if instances.is_empty() {
return Err(eyre::eyre!(
"No instances found in helix.toml. Add at least one instance to check."
));
}

// Check each instance
for instance_name in project.config.list_instances() {
print_status("CHECK", &format!("Validating instance '{instance_name}'"));
let _instance_config = project.config.get_instance(instance_name)?;
for instance_name in &instances {
check_instance(project, instance_name, metrics_sender).await?;
}

print_success("All instances are valid");
print_success("All instances checked successfully");
Ok(())
}



/// Validate project syntax by parsing queries and schema (similar to build.rs but without generating files)
fn validate_project_syntax(project: &ProjectContext) -> Result<()> {
print_status("VALIDATE", "Parsing and validating Helix queries");

// Collect all .hx files for validation
let hx_files = collect_hx_files(&project.root, &project.config.project.queries)?;

Expand All @@ -70,6 +152,68 @@ fn validate_project_syntax(project: &ProjectContext) -> Result<()> {
// Run static analysis to catch validation errors
analyze_source(source, &content.files)?;

print_success("All queries and schema are valid");
Ok(())
}

/// Run cargo check on the generated code.
fn run_cargo_check(helix_container_dir: &Path) -> Result<CargoCheckOutput> {
let output = Command::new("cargo")
.arg("check")
.arg("--color=never") // Disable color codes for cleaner output
.current_dir(helix_container_dir)
.output()
.map_err(|e| eyre::eyre!("Failed to run cargo check: {}", e))?;

let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
// stderr contains the actual errors, stdout has JSON if using message-format
let full_output = format!("{}\n{}", stderr, stdout);

let errors_only = filter_errors_only(&full_output);

Ok(CargoCheckOutput {
success: output.status.success(),
full_output,
errors_only,
})
}

/// Handle cargo check failure - print errors and offer GitHub issue creation.
fn handle_cargo_check_failure(
cargo_output: &CargoCheckOutput,
generated_rust: &str,
project: &ProjectContext,
) -> Result<()> {
print_error("Cargo check failed on generated Rust code");
println!();
println!("This may indicate a bug in the Helix code generator.");
println!();

// Offer to create GitHub issue
print_warning("You can report this issue to help improve Helix.");
println!();

let should_create = print_confirm("Would you like to create a GitHub issue with diagnostic information?")?;

if !should_create {
return Ok(());
}

// Collect .hx content
let hx_content = collect_hx_contents(&project.root, &project.config.project.queries)
.unwrap_or_else(|_| String::from("[Could not read .hx files]"));

// Build and open GitHub issue
let issue = GitHubIssueBuilder::new(cargo_output.errors_only.clone())
.with_hx_content(hx_content)
.with_generated_rust(generated_rust.to_string());

print_status("BROWSER", "Opening GitHub issue page...");
println!("Please review the content before submitting.");

issue.open_in_browser()?;

print_success("GitHub issue page opened in your browser");

Ok(())
}
Loading
Loading