Adding a Diagnostic
Diagnostics use a trait-based architecture. Each diagnostic pass lives in its own module under src/diagnostics/ and implements the DiagnosticPass trait. All diagnostic codes are defined centrally in src/diagnostics/mod.rs.
1. Define the diagnostic code
Add a DiagnosticDef constant to src/diagnostics/mod.rs:
pub(crate) const MY_NEW_CHECK: DiagnosticDef = DiagnosticDef {
code: "my-new-check",
severity: DiagnosticSeverity::WARNING,
};Also add it to the CATALOG array in the same file so it's recognized by the suppression and validation systems.
The code string is what users reference in @diagnostic disable:my-new-check and in .wowluarc.json — suppression works automatically by matching this string.
2. Create the module
Create src/diagnostics/my_new_check.rs. The DiagnosticPass trait has three methods — implement whichever fits your diagnostic:
run() — full-analysis pass
Best for diagnostics that walk the IR (symbols, functions, expressions) after type resolution:
use crate::analysis::AnalysisResult;
use crate::syntax::tree::SyntaxTree;
use super::{DiagnosticPass, WowDiagnostic};
pub(crate) struct MyNewCheck;
impl DiagnosticPass for MyNewCheck {
fn run(&self, analysis: &AnalysisResult, _tree: &SyntaxTree, diags: &mut Vec<WowDiagnostic>) {
// Walk IR data structures to find problems
for (sym_idx, symbol) in analysis.ir.symbols.iter().enumerate() {
if /* condition */ {
super::MY_NEW_CHECK.emit(
diags,
"description of what's wrong".to_string(),
start,
end,
);
}
}
}
}visit_node() — AST walk pass
Best for diagnostics that check syntax patterns. Called once per AST node during a shared tree walk:
use crate::analysis::AnalysisResult;
use crate::ast::{AstNode, BinaryExpression};
use crate::syntax::SyntaxKind;
use super::{DiagnosticPass, WowDiagnostic};
pub(crate) struct MyNewCheck;
impl DiagnosticPass for MyNewCheck {
fn visit_node(&self, node: crate::syntax::SyntaxNode<'_>, analysis: &AnalysisResult, diags: &mut Vec<WowDiagnostic>) {
if node.kind() != SyntaxKind::BinaryExpression { return; }
// Check the node and emit diagnostics
}
}run_inject() — inject-field pipeline
For diagnostics that participate in the type-mismatch → inject-field pipeline. Used by type_mismatch, return_mismatch, field_type_mismatch, assign_type_mismatch, and inject_field. You probably don't need this unless your diagnostic feeds the inject-field system.
3. Register the module
In src/diagnostics/mod.rs:
- Add
mod my_new_check;to the module declarations at the top - Add your pass to the appropriate list in
run_all():run_passes— forrun()implementations (most diagnostics)node_passes— forvisit_node()implementations (AST walks)inject_passes— forrun_inject()implementations (type-mismatch pipeline)
let run_passes: &[&dyn DiagnosticPass] = &[
// ... existing passes ...
&my_new_check::MyNewCheck,
];4. Add a test
Add test assertions to the appropriate test file. If your diagnostic is default-on, add it to tests/diagnostics/test.lua. If it's default-off, create a subdirectory with a .wowluarc.json that enables it.
-- Test that the diagnostic fires
local x = badThing()
-- ^ diag: my-new-check
-- Test that it doesn't fire on valid code
local y = goodThing()
-- ^ diag: noneRun cargo test to verify.
5. Document it
Add the diagnostic to the table in docs/reference/diagnostics.md.
Severity guidelines
| Severity | Use for |
|---|---|
| Warning | Likely bugs, type errors, annotation problems |
| Hint | Code quality suggestions, unused variables, style |
Default-off diagnostics
Some diagnostics are too noisy for unannotated codebases. Make them default-off by adding the code to DEFAULT_DISABLED_CODES in src/diagnostics/mod.rs. Users opt in via diagnostics.enable in .wowluarc.json.
Examples: need-check-nil, implicit-nil-return, unknown-param-type.
Hybrid modules
Some diagnostic modules are "hybrid" — they implement DiagnosticPass for the post-analysis phase AND export pub(crate) helper functions called from build_ir.rs or resolve.rs during IR construction. Both roles share the same DiagnosticDef constants from the catalog. This is used when a diagnostic needs to emit during IR construction (e.g. when specific AST context is available) and also during post-analysis passes.
