harness_sensors_rust/
lib.rs1use async_trait::async_trait;
8use harness_core::{
9 Action, CodeSpan, Execution, Sensor, SensorError, SensorId, Severity, Signal, Stage, World,
10};
11use serde::Deserialize;
12
13pub struct CargoCheck {
15 id: SensorId,
16}
17
18impl CargoCheck {
19 pub fn new() -> Self {
20 Self {
21 id: "cargo-check".into(),
22 }
23 }
24}
25
26impl Default for CargoCheck {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32#[async_trait]
33impl Sensor for CargoCheck {
34 fn id(&self) -> &SensorId {
35 &self.id
36 }
37 fn kind(&self) -> Execution {
38 Execution::Computational
39 }
40 fn stage(&self) -> Stage {
41 Stage::SelfCorrect
42 }
43
44 async fn observe(&self, _action: &Action, world: &World) -> Result<Vec<Signal>, SensorError> {
45 let out = world
46 .runner
47 .exec(
48 "cargo",
49 &["check", "--message-format=json", "--quiet"],
50 Some(world.repo.root.as_path()),
51 )
52 .await
53 .map_err(|e| SensorError::Failed {
54 id: self.id.clone(),
55 reason: e.to_string(),
56 })?;
57 Ok(parse_cargo_messages(&out.stdout, &self.id))
58 }
59}
60
61pub struct Clippy {
63 id: SensorId,
64}
65
66impl Clippy {
67 pub fn new() -> Self {
68 Self {
69 id: "clippy".into(),
70 }
71 }
72}
73
74impl Default for Clippy {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80#[async_trait]
81impl Sensor for Clippy {
82 fn id(&self) -> &SensorId {
83 &self.id
84 }
85 fn kind(&self) -> Execution {
86 Execution::Computational
87 }
88 fn stage(&self) -> Stage {
89 Stage::PreCommit
90 }
91
92 async fn observe(&self, _action: &Action, world: &World) -> Result<Vec<Signal>, SensorError> {
93 let out = world
94 .runner
95 .exec(
96 "cargo",
97 &[
98 "clippy",
99 "--message-format=json",
100 "--quiet",
101 "--",
102 "-D",
103 "warnings",
104 ],
105 Some(world.repo.root.as_path()),
106 )
107 .await
108 .map_err(|e| SensorError::Failed {
109 id: self.id.clone(),
110 reason: e.to_string(),
111 })?;
112 Ok(parse_cargo_messages(&out.stdout, &self.id))
113 }
114}
115
116#[derive(Debug, Deserialize)]
119struct CargoMsg {
120 reason: String,
121 #[serde(default)]
122 message: Option<RustcDiag>,
123}
124
125#[derive(Debug, Deserialize)]
126struct RustcDiag {
127 message: String,
128 level: String,
129 #[serde(default)]
130 spans: Vec<Span>,
131 #[serde(default)]
132 code: Option<DiagCode>,
133}
134
135#[derive(Debug, Deserialize)]
136struct Span {
137 file_name: String,
138 line_start: u32,
139 column_start: u32,
140 #[serde(default)]
141 is_primary: bool,
142}
143
144#[derive(Debug, Deserialize)]
145struct DiagCode {
146 code: String,
147}
148
149fn parse_cargo_messages(stdout: &str, origin: &str) -> Vec<Signal> {
150 let mut out = Vec::new();
151 for line in stdout.lines() {
152 let line = line.trim();
153 if line.is_empty() || !line.starts_with('{') {
154 continue;
155 }
156 let msg: CargoMsg = match serde_json::from_str(line) {
157 Ok(m) => m,
158 Err(_) => continue,
159 };
160 if msg.reason != "compiler-message" {
161 continue;
162 }
163 let diag = match msg.message {
164 Some(d) => d,
165 None => continue,
166 };
167 let severity = match diag.level.as_str() {
168 "error" | "error: internal compiler error" => Severity::Block,
169 "warning" => Severity::Warn,
170 _ => Severity::Hint,
171 };
172 let primary = diag
173 .spans
174 .iter()
175 .find(|s| s.is_primary)
176 .or_else(|| diag.spans.first());
177 let location = primary.map(|s| CodeSpan {
178 path: s.file_name.clone().into(),
179 line: s.line_start,
180 column: s.column_start,
181 length: 0,
182 });
183 let code_str = diag
184 .code
185 .as_ref()
186 .map(|c| format!(" [{}]", c.code))
187 .unwrap_or_default();
188 let agent_hint = build_hint(&diag.message, &diag.level);
189 out.push(Signal {
190 severity,
191 origin: origin.to_string(),
192 message: format!("{}{code_str}", diag.message),
193 agent_hint: Some(agent_hint),
194 auto_fix: None,
195 location,
196 });
197 }
198 out
199}
200
201fn build_hint(message: &str, level: &str) -> String {
202 match level {
203 "error" => {
204 format!("Fix this compilation error: {message}. Edit the file and re-run cargo check.")
205 }
206 "warning" => format!("Address this warning: {message}. Prefer fixing over silencing."),
207 _ => message.to_string(),
208 }
209}