1use crate::{BodyMatch, CodePattern, Relations};
6use ryo_symbol::SymbolId;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
13pub struct Rule {
14 pub id: String,
16
17 pub name: String,
19
20 pub severity: Severity,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub category: Option<String>,
26
27 pub query: PatternQuery,
29
30 pub message: String,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub suggestion: Option<String>,
36
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub scope: Vec<String>,
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub fix: Option<serde_json::Value>,
68}
69
70impl Rule {
71 pub fn new(
73 id: impl Into<String>,
74 name: impl Into<String>,
75 severity: Severity,
76 query: PatternQuery,
77 message: impl Into<String>,
78 ) -> Self {
79 Self {
80 id: id.into(),
81 name: name.into(),
82 severity,
83 category: None,
84 query,
85 message: message.into(),
86 suggestion: None,
87 scope: Vec::new(),
88 fix: None,
89 }
90 }
91
92 pub fn with_category(mut self, category: impl Into<String>) -> Self {
94 self.category = Some(category.into());
95 self
96 }
97
98 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
100 self.suggestion = Some(suggestion.into());
101 self
102 }
103
104 pub fn with_fix(mut self, fix: serde_json::Value) -> Self {
106 self.fix = Some(fix);
107 self
108 }
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
113pub struct PatternQuery {
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub kind: Option<SymbolKind>,
117
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub r#match: Option<MatchAttrs>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub body: Option<BodyMatch>,
125
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub relations: Option<Relations>,
129
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub pattern: Option<CodePattern>,
133}
134
135impl PatternQuery {
136 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn kind(mut self, kind: SymbolKind) -> Self {
143 self.kind = Some(kind);
144 self
145 }
146
147 pub fn with_match(mut self, attrs: MatchAttrs) -> Self {
149 self.r#match = Some(attrs);
150 self
151 }
152
153 pub fn with_body(mut self, body: BodyMatch) -> Self {
155 self.body = Some(body);
156 self
157 }
158
159 pub fn with_relations(mut self, relations: Relations) -> Self {
161 self.relations = Some(relations);
162 self
163 }
164
165 pub fn with_pattern(mut self, pattern: CodePattern) -> Self {
167 self.pattern = Some(pattern);
168 self
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
174pub enum SymbolKind {
175 Function,
177 Struct,
179 Enum,
181 Trait,
183 Impl,
185 Mod,
187 Const,
189 Static,
191 TypeAlias,
193 Field,
195 Variant,
197 CodePattern,
199}
200
201#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
203pub struct MatchAttrs {
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub name: Option<String>,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub pattern: Option<String>,
211
212 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub vis: Option<Visibility>,
215
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub trait_name: Option<String>,
219
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub attributes: Option<Vec<String>>,
223}
224
225impl MatchAttrs {
226 pub fn new() -> Self {
228 Self::default()
229 }
230
231 pub fn name(mut self, name: impl Into<String>) -> Self {
233 self.name = Some(name.into());
234 self
235 }
236
237 pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
239 self.pattern = Some(pattern.into());
240 self
241 }
242
243 pub fn vis(mut self, vis: Visibility) -> Self {
245 self.vis = Some(vis);
246 self
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
252pub enum Visibility {
253 Public,
255 Crate,
257 Super,
259 Private,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
265pub enum Severity {
266 Error,
268 Warning,
270 Info,
272 Hint,
274}
275
276impl std::fmt::Display for Severity {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 Severity::Error => write!(f, "error"),
280 Severity::Warning => write!(f, "warning"),
281 Severity::Info => write!(f, "info"),
282 Severity::Hint => write!(f, "hint"),
283 }
284 }
285}
286
287#[derive(Debug, Clone)]
289pub struct MatchResult {
290 pub matched: bool,
292
293 pub rule_id: Option<String>,
295
296 pub severity: Option<Severity>,
298
299 pub message: Option<String>,
301
302 pub suggestion: Option<String>,
304
305 pub captures: HashMap<String, CapturedNode>,
307}
308
309impl MatchResult {
310 pub fn no_match() -> Self {
312 Self {
313 matched: false,
314 rule_id: None,
315 severity: None,
316 message: None,
317 suggestion: None,
318 captures: HashMap::new(),
319 }
320 }
321
322 pub fn matched() -> Self {
324 Self {
325 matched: true,
326 rule_id: None,
327 severity: None,
328 message: None,
329 suggestion: None,
330 captures: HashMap::new(),
331 }
332 }
333
334 pub fn with_rule(mut self, rule: &Rule) -> Self {
336 self.rule_id = Some(rule.id.clone());
337 self.severity = Some(rule.severity);
338 self.message = Some(rule.message.clone());
339 self.suggestion = rule.suggestion.clone();
340 self
341 }
342
343 pub fn capture(mut self, var: impl Into<String>, node: CapturedNode) -> Self {
345 self.captures.insert(var.into(), node);
346 self
347 }
348}
349
350#[derive(Debug, Clone)]
352pub struct CapturedNode {
353 pub symbol_id: Option<SymbolId>,
355
356 pub span: Span,
358
359 pub text: String,
361}
362
363impl CapturedNode {
364 pub fn new(span: Span, text: impl Into<String>) -> Self {
366 Self {
367 symbol_id: None,
368 span,
369 text: text.into(),
370 }
371 }
372
373 pub fn with_symbol(mut self, id: SymbolId) -> Self {
375 self.symbol_id = Some(id);
376 self
377 }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub struct Span {
383 pub start: Position,
385 pub end: Position,
387}
388
389impl Span {
390 pub fn new(start: Position, end: Position) -> Self {
392 Self { start, end }
393 }
394
395 pub fn point(line: u32, column: u32) -> Self {
397 let pos = Position { line, column };
398 Self {
399 start: pos,
400 end: pos,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub struct Position {
408 pub line: u32,
410 pub column: u32,
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::NodeKind;
418
419 #[test]
420 fn test_rule_builder() {
421 let query = PatternQuery::new().kind(SymbolKind::Function).with_match(
422 MatchAttrs::new()
423 .vis(Visibility::Public)
424 .pattern("process_*"),
425 );
426
427 let rule = Rule::new(
428 "RL001",
429 "no-unwrap",
430 Severity::Warning,
431 query,
432 "Avoid unwrap()",
433 )
434 .with_category("error-handling")
435 .with_suggestion("Use ? operator instead");
436
437 assert_eq!(rule.id, "RL001");
438 assert_eq!(rule.severity, Severity::Warning);
439 assert!(rule.category.is_some());
440 assert!(rule.suggestion.is_some());
441 }
442
443 #[test]
444 fn test_match_result() {
445 let result = MatchResult::matched().capture(
446 "$UNWRAP",
447 CapturedNode::new(Span::point(42, 10), "result.unwrap()"),
448 );
449
450 assert!(result.matched);
451 assert!(result.captures.contains_key("$UNWRAP"));
452 }
453
454 #[test]
455 fn test_pattern_query_with_body() {
456 let query = PatternQuery::new()
457 .kind(SymbolKind::Function)
458 .with_body(BodyMatch::new().contains(CodePattern::new(NodeKind::MethodCall)));
459
460 assert!(query.kind.is_some());
461 assert!(query.body.is_some());
462 }
463}