hedl-lint
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.
What's Implemented
Comprehensive linting with configuration and security:
- 4 Lint Rules: ID naming, unused schemas, empty lists, unqualified references
- Three Severity Levels: Hint (informational), Warning (should fix), Error (must fix)
- Configurable Rules: Enable/disable individual rules, escalate to error via config
- Rule-Level Escalation: Promote individual rule diagnostics to error via config
- Custom Rules: LintRule trait for user-defined lint checks
- Security Limits: Max recursion depth (1000), max diagnostics (10,000)
- Line Number Tracking: Diagnostics support optional line numbers
- CLI Integration: Used by
hedl lintcommand with multiple output formats - IDE Integration: Powers diagnostics in hedl-lsp for real-time feedback
- Performance: O(n) single pass through document, minimal overhead
Installation
[]
= "2.0"
Basic Usage
Lint with Default Rules
use parse;
use lint;
let doc = parse?;
let diagnostics = lint;
for diag in &diagnostics
Output:
[unused-schema] warning: Schema 'Product' is defined but never used
Custom Configuration
use ;
let mut config = default;
config.disable_rule;
config.set_rule_error;
config.min_severity = Warning;
let diagnostics = lint_with_config;
Lint Rules
1. id-naming (Hint, enabled)
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:
- Consistent naming style (kebab-case, snake_case, camelCase)
- No mixed naming styles within document
- Descriptive IDs (warns on generic like "item1", "id1")
Severity: Hint (informational, doesn't fail builds)
Configuration:
let mut config = default;
// Enable by default, or disable:
config.disable_rule;
2. unused-schema (Warning, enabled)
Detects %STRUCT definitions that are never referenced:
%V:2.0
%S:User:[id, name, email]
%S:Product:[id, title, price]
%S:Order:[id, customer, total]
---
users: @User
| alice, Alice, alice@example.com
# Warning: Product and Order schemas defined but unused
Impact:
- Clutters header with unnecessary declarations
- Confuses readers about expected document structure
- Increases file size and parsing overhead
Severity: Warning (should be fixed)
Fix: Remove unused %STRUCT declarations or add corresponding entity lists
Configuration:
let mut config = default;
// Escalate to error for strict validation:
config.set_rule_error;
3. empty-list (Hint, enabled)
Flags matrix lists with schema but zero rows:
%V:2.0
%S:User:[id, name, age]
---
users: @User[0]
# No rows - empty list
# Hint: Empty list 'users' defined but contains no data
When This Occurs:
- Placeholder lists during development
- Filtered results with no matches
- Template documents
Why It Matters:
- Wastes bytes in production data
- May indicate incomplete data export
- Confuses document purpose
Severity: Hint (may be intentional)
Configuration:
let mut config = default;
// Escalate to error for strict validation:
config.set_rule_error;
4. unqualified-kv-ref (Warning, enabled)
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:
- Explicit type information aids tooling (LSP, validation)
- Self-documenting references
- Enables static analysis
- Prevents ambiguity when multiple entity types have same ID
Severity: Warning (impacts code quality)
Configuration:
let mut config = default;
// Enabled by default with Warning severity
Severity Levels
Hint
Purpose: Informational suggestions for improvements
Examples:
- ID naming conventions
- Empty lists (may be intentional)
- Stylistic preferences
Behavior:
- Does NOT fail CI/CD builds
- Shown in IDE with blue squiggle
- Optional to fix
Warning
Purpose: Should be fixed but not blocking
Examples:
- Unused schemas
- Unqualified references in key-value context
- Unused aliases
Behavior:
- Fails strict validation modes
- Shown in IDE with yellow squiggle
- Should be addressed before merge
Error
Purpose: Must be fixed, blocks deployment
Examples:
- Schema violations (wrong column count)
- Unresolved references (when escalated)
- Critical structural issues
Behavior:
- Always fails validation
- Shown in IDE with red squiggle
- Blocks CI/CD pipeline
Configuration System
LintConfig
use ;
let mut config = default;
// Enable/disable individual rules
config.enable_rule;
config.disable_rule;
// Escalate specific rules to error
config.set_rule_error;
// Set minimum severity to report
config.min_severity = Warning;
// Set maximum diagnostics to collect (default: 10,000)
config.max_diagnostics = 5000;
RuleConfig
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 severity
Example:
use RuleConfig;
let rule_config = RuleConfig ;
Per-Rule Escalation
The set_rule_error method enables a rule and escalates both Hint and Warning diagnostics to Error:
let mut config = default;
config.set_rule_error;
// Now all unused-schema diagnostics become errors (fail fast)
Custom Rules
Implement the LintRule trait for custom checks:
use ;
use Document;
;
// Use custom rule
use ;
let mut runner = new;
runner.add_rule;
let diagnostics = runner.run;
LintRule Trait
Security Limits
Recursion Depth Limit
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:
- Stack overflow from malicious input
- Infinite recursion bugs
- Runaway memory consumption
Diagnostic Count Limit
Protection against diagnostic explosion:
const MAX_DIAGNOSTICS: usize = 10_000;
// If linting generates > 10,000 diagnostics:
// Warning diagnostic added, further diagnostics suppressed
Prevents:
- Memory exhaustion from severely malformed input
- Unbounded diagnostic generation
- DoS attacks via pathological documents
Diagnostic Structure
The lint and lint_with_config functions return Vec<Diagnostic> directly (no Result wrapper), as linting does not fail on malformed documents:
use lint;
let diagnostics = lint;
for diag in &diagnostics
Diagnostic Fields
Diagnostics are accessed through getter methods:
let diag = &diagnostics;
println!;
println!;
println!;
println!;
if let Some = diag.line
if let Some = diag.suggestion
DiagnosticKind Enum
The DiagnosticKind enum identifies the type of lint issue:
Example:
use ;
let diag = warning.with_line;
println!;
// Output: line 5: [unused-schema] warning: Schema 'Product' defined but never used
CLI Integration
The hedl-cli crate uses hedl-lint for the lint command:
# Lint with default rules
# JSON output for tooling
# Escalate warnings to errors
# Disable specific rules
Exit Codes:
- 0: No errors (hints and warnings allowed)
- 1: One or more errors found
- 2: Internal linter error
IDE Integration
The hedl-lsp crate uses hedl-lint for real-time diagnostics:
Features:
- Live linting as you type (200ms debounce)
- Squiggly underlines (blue/yellow/red for hint/warning/error)
- Hover messages with full diagnostic text
- Quick fixes (where applicable)
Performance: Incremental linting on document change, cached between edits.
Use Cases
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.
What This Crate Doesn't Do
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.
Performance Characteristics
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.
Dependencies
hedl-core2.0 - Core HEDL data structures and parsingthiserror1.0 - Error type definitions
License
Apache-2.0