| Crates.io | hedl-lint |
| lib.rs | hedl-lint |
| version | 1.2.0 |
| created_at | 2026-01-08 11:11:23.97957+00 |
| updated_at | 2026-01-21 03:00:52.642615+00 |
| description | HEDL linting and best practices validation |
| homepage | https://dweve.com |
| repository | https://github.com/dweve/hedl |
| max_upload_size | |
| id | 2030000 |
| size | 444,629 |
Production-grade linting for HEDL documents—catch errors, enforce best practices, and improve code quality before deployment.
Valid syntax isn't enough. Unused schemas clutter headers. Empty lists waste space. Unqualified references in key-value contexts lose type information. ID fields should follow conventions. Code reviews catch these issues too late. Automated linting enforces standards consistently across teams and prevents common mistakes from reaching production.
hedl-lint provides comprehensive linting with 4 configurable rules covering naming conventions, schema usage, reference quality, and structural best practices. Integrates seamlessly with hedl-cli, LSP, and CI/CD pipelines. Configurable severity levels (Hint/Warning/Error) with per-rule error escalation. Custom rule support via trait system. Security-hardened with recursion and diagnostic limits.
Comprehensive linting with configuration and security:
hedl lint command with multiple output formats[dependencies]
hedl-lint = "1.2"
use hedl_core::parse;
use hedl_lint::lint;
let doc = parse(br#"
%VERSION: 1.0
%STRUCT: User: [id, name, email]
%STRUCT: Product: [id, title, price]
---
users: @User
| alice, Alice, alice@example.com
| bob, Bob, bob@example.com
products: @Item
| item1, Widget, 9.99
"#)?;
let diagnostics = lint(&doc);
for diag in &diagnostics {
println!("{}", diag);
}
Output:
[unused-schema] warning: Schema 'Product' is defined but never used
use hedl_lint::{lint_with_config, LintConfig, Severity};
let mut config = LintConfig::default();
config.disable_rule("id-naming");
config.set_rule_error("unused-schema");
config.min_severity = Severity::Warning;
let diagnostics = lint_with_config(&doc, config);
Checks ID field naming conventions for consistency:
# Good: kebab-case IDs
users: @User[id, name]
| alice-smith, Alice Smith
| bob-jones, Bob Jones
# Warning: inconsistent casing
users: @User[id, name]
| AliceSmith, Alice Smith # Hint: prefer kebab-case
| bob_jones, Bob Jones # Hint: prefer kebab-case
Checks:
Severity: Hint (informational, doesn't fail builds)
Configuration:
let mut config = LintConfig::default();
// Enable by default, or disable:
config.disable_rule("id-naming");
Detects %STRUCT definitions that are never referenced:
%VERSION: 1.0
%STRUCT: User: [id, name, email]
%STRUCT: Product: [id, title, price]
%STRUCT: Order: [id, customer, total]
---
users: @User
| alice, Alice, alice@example.com
# Warning: Product and Order schemas defined but unused
Impact:
Severity: Warning (should be fixed)
Fix: Remove unused %STRUCT declarations or add corresponding entity lists
Configuration:
let mut config = LintConfig::default();
// Escalate to error for strict validation:
config.set_rule_error("unused-schema");
Flags matrix lists with schema but zero rows:
%VERSION: 1.0
%STRUCT: User: [id, name, age]
---
users: @User[0]
# No rows - empty list
# Hint: Empty list 'users' defined but contains no data
When This Occurs:
Why It Matters:
Severity: Hint (may be intentional)
Configuration:
let mut config = LintConfig::default();
// Escalate to error for strict validation:
config.set_rule_error("empty-list");
Warns about unqualified references in key-value context:
# Bad: unqualified reference loses type information
config:
admin: @alice # Warning: prefer @User:alice
# Good: qualified reference preserves type
config:
admin: @User:alice # Type information explicit
Why Qualified is Better:
Severity: Warning (impacts code quality)
Configuration:
let mut config = LintConfig::default();
// Enabled by default with Warning severity
Purpose: Informational suggestions for improvements
Examples:
Behavior:
Purpose: Should be fixed but not blocking
Examples:
Behavior:
Purpose: Must be fixed, blocks deployment
Examples:
Behavior:
use hedl_lint::{LintConfig, Severity};
let mut config = LintConfig::default();
// Enable/disable individual rules
config.enable_rule("id-naming");
config.disable_rule("empty-list");
// Escalate specific rules to error
config.set_rule_error("unused-schema");
// Set minimum severity to report
config.min_severity = Severity::Warning;
// Set maximum diagnostics to collect (default: 10,000)
config.max_diagnostics = 5000;
Rule configuration is stored in a HashMap and has two fields:
enabled: bool - Whether the rule is activeerror: bool - Whether to escalate all diagnostics from this rule to Error severityExample:
use hedl_lint::rules::RuleConfig;
let rule_config = RuleConfig {
enabled: true,
error: true, // Escalate this rule's diagnostics to Error
};
The set_rule_error method enables a rule and escalates both Hint and Warning diagnostics to Error:
let mut config = LintConfig::default();
config.set_rule_error("unused-schema");
// Now all unused-schema diagnostics become errors (fail fast)
Implement the LintRule trait for custom checks:
use hedl_lint::{LintRule, Diagnostic, DiagnosticKind};
use hedl_core::Document;
pub struct RequireDescriptionRule;
impl LintRule for RequireDescriptionRule {
fn id(&self) -> &str {
"require-description"
}
fn description(&self) -> &str {
"Check that document has a description field"
}
fn check(&self, doc: &Document) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
// Check if document has 'description' field
if !doc.root.contains_key("description") {
diagnostics.push(
Diagnostic::warning(
DiagnosticKind::Custom("require-description".to_string()),
"Document should have a 'description' field",
"require-description"
).with_line(1)
);
}
diagnostics
}
}
// Use custom rule
use hedl_lint::{LintRunner, LintConfig};
let mut runner = LintRunner::new(LintConfig::default());
runner.add_rule(Box::new(RequireDescriptionRule));
let diagnostics = runner.run(&doc);
pub trait LintRule: Send + Sync {
/// Unique rule identifier (kebab-case)
fn id(&self) -> &str;
/// Rule description
fn description(&self) -> &str;
/// Check document and return diagnostics
fn check(&self, doc: &Document) -> Vec<Diagnostic>;
}
Protection against deeply nested structures:
const MAX_RECURSION_DEPTH: usize = 1000;
// Linting document > 1000 levels deep:
// Warning diagnostic added, traversal stops at that depth
Prevents:
Protection against diagnostic explosion:
const MAX_DIAGNOSTICS: usize = 10_000;
// If linting generates > 10,000 diagnostics:
// Warning diagnostic added, further diagnostics suppressed
Prevents:
The lint and lint_with_config functions return Vec<Diagnostic> directly (no Result wrapper), as linting does not fail on malformed documents:
use hedl_lint::lint;
let diagnostics = lint(&doc);
for diag in &diagnostics {
println!("{}", diag);
}
Diagnostics are accessed through getter methods:
let diag = &diagnostics[0];
println!("Severity: {:?}", diag.severity());
println!("Kind: {:?}", diag.kind());
println!("Message: {}", diag.message());
println!("Rule ID: {}", diag.rule_id());
if let Some(line) = diag.line() {
println!("Line: {}", line);
}
if let Some(suggestion) = diag.suggestion() {
println!("Suggestion: {}", suggestion);
}
The DiagnosticKind enum identifies the type of lint issue:
pub enum DiagnosticKind {
/// ID naming convention violation
IdNaming,
/// Unused schema definition
UnusedSchema,
/// Empty matrix list
EmptyList,
/// Unqualified reference in Key-Value context
UnqualifiedKvReference,
/// Custom rule violation
Custom(String),
}
Example:
use hedl_lint::{Diagnostic, DiagnosticKind, Severity};
let diag = Diagnostic::warning(
DiagnosticKind::UnusedSchema,
"Schema 'Product' defined but never used",
"unused-schema"
).with_line(5);
println!("{}", diag);
// Output: line 5: [unused-schema] warning: Schema 'Product' defined but never used
The hedl-cli crate uses hedl-lint for the lint command:
# Lint with default rules
hedl lint document.hedl
# JSON output for tooling
hedl lint --format json document.hedl
# Escalate warnings to errors
hedl lint --strict document.hedl
# Disable specific rules
hedl lint --disable unused-alias document.hedl
Exit Codes:
The hedl-lsp crate uses hedl-lint for real-time diagnostics:
Features:
Performance: Incremental linting on document change, cached between edits.
CI/CD Validation: Run hedl lint in CI pipelines to enforce code quality standards. Fail builds on warnings in strict mode.
Pre-Commit Hooks: Add linting to git pre-commit hooks to catch issues before they reach code review. Faster feedback loop.
Code Review Automation: Reduce human review burden by catching common issues automatically. Let reviewers focus on logic, not style.
IDE Integration: Real-time feedback while editing prevents issues from being introduced. Fix problems immediately at creation time.
Migration Tooling: Run linting on legacy HEDL to identify quality issues during modernization projects. Generate technical debt inventory.
Documentation Generation: Use lint diagnostics to generate quality reports for stakeholders. Track improvement over time with metrics.
Schema Validation: Linting checks best practices, not structural validity. For schema validation (column count, type checking), use hedl-core's validator.
Auto-Fixing: Diagnostics identify problems but don't automatically fix them. For formatting, use hedl-c14n or hedl format.
Performance Profiling: Linting focuses on code quality, not runtime performance. For performance analysis, use hedl-bench.
Security Scanning: While security-hardened, this isn't a security scanner. For vulnerability detection in dependencies, use cargo-audit.
Time Complexity: O(n) where n = total entities + fields. Single linear pass through document.
Space Complexity: O(d) where d = diagnostic count. Typically <100 diagnostics per document.
Caching: Lint results can be cached by document hash for repeated checks (implemented in hedl-lsp).
Overhead: Minimal (<5% of parse time). Suitable for interactive use in IDEs.
hedl-core 1.2 - Core HEDL data structures and parsingthiserror 1.0 - Error type definitionsApache-2.0