use crate::{BodyMatch, CodePattern, Relations};
use ryo_symbol::SymbolId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Rule {
pub id: String,
pub name: String,
pub severity: Severity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
pub query: PatternQuery,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scope: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fix: Option<serde_json::Value>,
}
impl Rule {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
severity: Severity,
query: PatternQuery,
message: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
severity,
category: None,
query,
message: message.into(),
suggestion: None,
scope: Vec::new(),
fix: None,
}
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn with_fix(mut self, fix: serde_json::Value) -> Self {
self.fix = Some(fix);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PatternQuery {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<SymbolKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#match: Option<MatchAttrs>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<BodyMatch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relations: Option<Relations>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<CodePattern>,
}
impl PatternQuery {
pub fn new() -> Self {
Self::default()
}
pub fn kind(mut self, kind: SymbolKind) -> Self {
self.kind = Some(kind);
self
}
pub fn with_match(mut self, attrs: MatchAttrs) -> Self {
self.r#match = Some(attrs);
self
}
pub fn with_body(mut self, body: BodyMatch) -> Self {
self.body = Some(body);
self
}
pub fn with_relations(mut self, relations: Relations) -> Self {
self.relations = Some(relations);
self
}
pub fn with_pattern(mut self, pattern: CodePattern) -> Self {
self.pattern = Some(pattern);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Mod,
Const,
Static,
TypeAlias,
Field,
Variant,
CodePattern,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct MatchAttrs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vis: Option<Visibility>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trait_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes: Option<Vec<String>>,
}
impl MatchAttrs {
pub fn new() -> Self {
Self::default()
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn vis(mut self, vis: Visibility) -> Self {
self.vis = Some(vis);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Visibility {
Public,
Crate,
Super,
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
Severity::Info => write!(f, "info"),
Severity::Hint => write!(f, "hint"),
}
}
}
#[derive(Debug, Clone)]
pub struct MatchResult {
pub matched: bool,
pub rule_id: Option<String>,
pub severity: Option<Severity>,
pub message: Option<String>,
pub suggestion: Option<String>,
pub captures: HashMap<String, CapturedNode>,
}
impl MatchResult {
pub fn no_match() -> Self {
Self {
matched: false,
rule_id: None,
severity: None,
message: None,
suggestion: None,
captures: HashMap::new(),
}
}
pub fn matched() -> Self {
Self {
matched: true,
rule_id: None,
severity: None,
message: None,
suggestion: None,
captures: HashMap::new(),
}
}
pub fn with_rule(mut self, rule: &Rule) -> Self {
self.rule_id = Some(rule.id.clone());
self.severity = Some(rule.severity);
self.message = Some(rule.message.clone());
self.suggestion = rule.suggestion.clone();
self
}
pub fn capture(mut self, var: impl Into<String>, node: CapturedNode) -> Self {
self.captures.insert(var.into(), node);
self
}
}
#[derive(Debug, Clone)]
pub struct CapturedNode {
pub symbol_id: Option<SymbolId>,
pub span: Span,
pub text: String,
}
impl CapturedNode {
pub fn new(span: Span, text: impl Into<String>) -> Self {
Self {
symbol_id: None,
span,
text: text.into(),
}
}
pub fn with_symbol(mut self, id: SymbolId) -> Self {
self.symbol_id = Some(id);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: Position,
pub end: Position,
}
impl Span {
pub fn new(start: Position, end: Position) -> Self {
Self { start, end }
}
pub fn point(line: u32, column: u32) -> Self {
let pos = Position { line, column };
Self {
start: pos,
end: pos,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub line: u32,
pub column: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NodeKind;
#[test]
fn test_rule_builder() {
let query = PatternQuery::new().kind(SymbolKind::Function).with_match(
MatchAttrs::new()
.vis(Visibility::Public)
.pattern("process_*"),
);
let rule = Rule::new(
"RL001",
"no-unwrap",
Severity::Warning,
query,
"Avoid unwrap()",
)
.with_category("error-handling")
.with_suggestion("Use ? operator instead");
assert_eq!(rule.id, "RL001");
assert_eq!(rule.severity, Severity::Warning);
assert!(rule.category.is_some());
assert!(rule.suggestion.is_some());
}
#[test]
fn test_match_result() {
let result = MatchResult::matched().capture(
"$UNWRAP",
CapturedNode::new(Span::point(42, 10), "result.unwrap()"),
);
assert!(result.matched);
assert!(result.captures.contains_key("$UNWRAP"));
}
#[test]
fn test_pattern_query_with_body() {
let query = PatternQuery::new()
.kind(SymbolKind::Function)
.with_body(BodyMatch::new().contains(CodePattern::new(NodeKind::MethodCall)));
assert!(query.kind.is_some());
assert!(query.body.is_some());
}
}