///|
/// A severity attached to an analysis finding.
pub(all) enum Severity {
Info
Warning
Error
} derive(Eq, @debug.Debug)
///|
/// The family of rule that produced a finding.
pub(all) enum IssueKind {
ExactConflict
PrefixConflict
ShadowedBinding
DuplicateCommand
ReservedShortcut
AccessibilityRisk
InvalidKey
InvalidContext
InvalidRecord
DisabledBinding
PlatformOverlap
} derive(Eq, @debug.Debug)
///|
/// A normalized keyboard sequence. The textual form is stable and suitable for
/// reports, while `steps` keeps each chord available to library consumers.
pub(all) struct KeySequence {
raw : String
steps : Array[String]
canonical : String
has_modifier : Bool
modifier_count : Int
key_count : Int
} derive(Eq, @debug.Debug)
///|
/// A single shortcut declaration from a keymap file.
pub(all) struct Binding {
id : String
command : String
keys : KeySequence
context : String
platform : String
source : String
line : Int
priority : Int
enabled : Bool
description : String
} derive(Eq, @debug.Debug)
///|
/// A context declaration. Contexts form a small inheritance tree used when
/// deciding whether one binding shadows another.
pub(all) struct Context {
name : String
parent : String
rank : Int
description : String
} derive(Eq, @debug.Debug)
///|
/// A finding emitted by the analyzer.
pub(all) struct Finding {
code : String
kind : IssueKind
severity : Severity
message : String
primary_id : String
secondary_id : String
shortcut : String
context : String
source : String
line : Int
suggestion : String
} derive(Eq, @debug.Debug)
///|
/// All data needed to analyze one keymap.
pub(all) struct Keymap {
name : String
version : String
bindings : Array[Binding]
contexts : Array[Context]
reserved : Array[String]
} derive(Eq, @debug.Debug)
///|
/// A complete deterministic analysis result.
pub(all) struct Analysis {
keymap_name : String
findings : Array[Finding]
checked_bindings : Int
enabled_bindings : Int
error_count : Int
warning_count : Int
info_count : Int
score : Int
fingerprint : String
} derive(Eq, @debug.Debug)
///|
/// A candidate replacement suggested for a problematic binding.
pub(all) struct Suggestion {
binding_id : String
command : String
current : String
replacement : String
reason : String
confidence : Int
} derive(Eq, @debug.Debug)
///|
/// Parse diagnostics are kept separate from policy findings.
pub(all) struct ParseDiagnostic {
line : Int
code : String
message : String
source : String
} derive(Eq, @debug.Debug)
///|
/// Result returned by the line-oriented keymap parser.
pub(all) struct ParseResult {
keymap : Keymap
diagnostics : Array[ParseDiagnostic]
ok : Bool
} derive(Eq, @debug.Debug)
///|
/// Construct an empty keymap.
pub fn Keymap::empty(name? : String = "keymap") -> Keymap {
{
name,
version: "1",
bindings: [],
contexts: [
{ name: "global", parent: "", rank: 0, description: "all windows" },
],
reserved: [],
}
}
///|
/// Construct a normalized sequence value.
pub fn KeySequence::new(
raw : String,
steps : Array[String],
canonical : String,
has_modifier : Bool,
modifier_count : Int,
) -> KeySequence {
{
raw,
steps,
canonical,
has_modifier,
modifier_count,
key_count: steps.length(),
}
}
///|
/// Construct a binding with sensible defaults for programmatic callers.
pub fn Binding::new(
id : String,
command : String,
keys : KeySequence,
context? : String = "global",
platform? : String = "all",
source? : String = "api",
line? : Int = 0,
priority? : Int = 0,
enabled? : Bool = true,
description? : String = "",
) -> Binding {
{
id,
command,
keys,
context,
platform,
source,
line,
priority,
enabled,
description,
}
}
///|
/// Return the stable identifier used in reports.
pub fn Binding::label(self : Binding) -> String {
if self.source.length() == 0 {
self.id
} else {
self.source + ":" + self.line.to_string() + ":" + self.id
}
}
///|
/// Return the issue severity as a machine-readable string.
pub fn Severity::name(self : Severity) -> String {
match self {
Info => "info"
Warning => "warning"
Error => "error"
}
}
///|
/// Return the issue kind as a stable kebab-case code.
pub fn IssueKind::name(self : IssueKind) -> String {
match self {
ExactConflict => "exact-conflict"
PrefixConflict => "prefix-conflict"
ShadowedBinding => "shadowed-binding"
DuplicateCommand => "duplicate-command"
ReservedShortcut => "reserved-shortcut"
AccessibilityRisk => "accessibility-risk"
InvalidKey => "invalid-key"
InvalidContext => "invalid-context"
InvalidRecord => "invalid-record"
DisabledBinding => "disabled-binding"
PlatformOverlap => "platform-overlap"
}
}
///|
/// Convert a finding into a compact human-readable line.
pub fn Finding::to_line(self : Finding) -> String {
let location = if self.source.length() == 0 {
""
} else {
self.source + ":" + self.line.to_string()
}
let pair = if self.secondary_id.length() == 0 {
self.primary_id
} else {
self.primary_id + " vs " + self.secondary_id
}
self.severity.name().to_upper() +
" " +
self.code +
" " +
location +
" " +
pair +
": " +
self.message
}
///|
/// True when the analysis has no error-level findings.
pub fn Analysis::ok(self : Analysis) -> Bool {
self.error_count == 0
}
///|
/// Count findings of a particular kind.
pub fn Analysis::count_kind(self : Analysis, kind : IssueKind) -> Int {
let mut total = 0
for finding in self.findings {
if finding.kind == kind {
total += 1
}
}
total
}
///|
/// Return a compact summary intended for CI logs.
pub fn Analysis::summary(self : Analysis) -> String {
self.keymap_name +
": " +
self.checked_bindings.to_string() +
" bindings, " +
self.error_count.to_string() +
" errors, " +
self.warning_count.to_string() +
" warnings, score " +
self.score.to_string() +
"/100, fingerprint " +
self.fingerprint
}