use crate::fixer::Fixer;
use crate::rule::{RuleSerializeError, SerializableRule};
use crate::transform::Transformation;
use crate::DeserializeEnv;
use crate::GlobalRules;
pub use crate::constraints::{
try_deserialize_matchers, RuleWithConstraint, SerializableMetaVarMatcher,
SerializeConstraintsError,
};
use ast_grep_core::language::Language;
use ast_grep_core::meta_var::MetaVarMatchers;
use ast_grep_core::replacer::{IndentSensitive, Replacer};
use ast_grep_core::replacer::{TemplateFix, TemplateFixError};
use ast_grep_core::{NodeMatch, StrDoc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_yaml::{with::singleton_map_recursive::deserialize, Deserializer, Error as YamlError};
use thiserror::Error;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
#[derive(Serialize, Deserialize, Clone, Default, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Severity {
#[default]
Hint,
Info,
Warning,
Error,
Off,
}
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
pub struct SerializableRuleCore<L: Language> {
pub language: L,
pub rule: SerializableRule,
pub constraints: Option<HashMap<String, SerializableMetaVarMatcher>>,
pub utils: Option<HashMap<String, SerializableRule>>,
pub transform: Option<HashMap<String, Transformation>>,
}
impl<L: Language> SerializableRuleCore<L> {
fn get_deserialize_env(&self, globals: &GlobalRules<L>) -> RResult<DeserializeEnv<L>> {
let env = DeserializeEnv::new(self.language.clone()).with_globals(globals);
if let Some(utils) = &self.utils {
let env = env.register_local_utils(utils)?;
Ok(env)
} else {
Ok(env)
}
}
fn get_meta_var_matchers(&self) -> RResult<MetaVarMatchers<StrDoc<L>>> {
Ok(if let Some(constraints) = self.constraints.clone() {
try_deserialize_matchers(constraints, self.language.clone())?
} else {
MetaVarMatchers::default()
})
}
pub fn get_matcher(&self, globals: &GlobalRules<L>) -> RResult<RuleWithConstraint<L>> {
let env = self.get_deserialize_env(globals)?;
let rule = env.deserialize_rule(self.rule.clone())?;
let matchers = self.get_meta_var_matchers()?;
let transform = self.transform.clone();
Ok(
RuleWithConstraint::new(rule)
.with_matchers(matchers)
.with_utils(env.registration)
.with_transform(transform),
)
}
}
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
pub struct SerializableRuleConfigCore<L: Language> {
#[serde(flatten)]
pub core: SerializableRuleCore<L>,
pub id: String,
}
pub fn into_map<L: Language>(
rules: Vec<SerializableRuleConfigCore<L>>,
) -> HashMap<String, SerializableRuleCore<L>> {
rules.into_iter().map(|r| (r.id, r.core)).collect()
}
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
pub struct SerializableRuleConfig<L: Language> {
#[serde(flatten)]
pub core: SerializableRuleCore<L>,
pub id: String,
#[serde(default)]
pub message: String,
pub note: Option<String>,
#[serde(default)]
pub severity: Severity,
pub fix: Option<Fixer>,
pub files: Option<Vec<String>>,
pub ignores: Option<Vec<String>>,
pub url: Option<String>,
pub metadata: Option<HashMap<String, String>>,
}
type RResult<T> = std::result::Result<T, RuleConfigError>;
impl<L: Language> SerializableRuleConfig<L> {
pub fn get_fixer<C: IndentSensitive>(&self) -> RResult<Option<TemplateFix<C>>> {
if let Some(Fixer::Str(fix)) = &self.fix {
if let Some(trans) = &self.transform {
let keys: Vec<_> = trans.keys().cloned().collect();
Ok(Some(TemplateFix::with_transform(
fix,
&self.language,
&keys,
)))
} else {
Ok(Some(TemplateFix::try_new(fix, &self.language)?))
}
} else {
Ok(None)
}
}
fn get_message(&self, node_match: &NodeMatch<StrDoc<L>>) -> String {
let bytes = self.message.generate_replacement(node_match);
String::from_utf8(bytes).expect("replacement must be valid utf-8")
}
}
impl<L: Language> Deref for SerializableRuleConfig<L> {
type Target = SerializableRuleCore<L>;
fn deref(&self) -> &Self::Target {
&self.core
}
}
impl<L: Language> DerefMut for SerializableRuleConfig<L> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.core
}
}
#[derive(Debug, Error)]
pub enum RuleConfigError {
#[error("Fail to parse yaml as RuleConfig")]
Yaml(#[from] YamlError),
#[error("Rule is not configured correctly.")]
Rule(#[from] RuleSerializeError),
#[error("fix pattern is invalid.")]
Fixer(#[from] TemplateFixError),
#[error("constraints is not configured correctly.")]
Constraints(#[from] SerializeConstraintsError),
}
pub struct RuleConfig<L: Language> {
inner: SerializableRuleConfig<L>,
pub matcher: RuleWithConstraint<L>,
pub fixer: Option<TemplateFix<String>>,
}
impl<L: Language> RuleConfig<L> {
pub fn try_from(
inner: SerializableRuleConfig<L>,
globals: &GlobalRules<L>,
) -> Result<Self, RuleConfigError> {
let matcher = inner.get_matcher(globals)?;
let fixer = inner.get_fixer()?;
Ok(Self {
inner,
matcher,
fixer,
})
}
pub fn deserialize<'de>(
deserializer: Deserializer<'de>,
globals: &GlobalRules<L>,
) -> Result<Self, RuleConfigError>
where
L: Deserialize<'de>,
{
let inner: SerializableRuleConfig<L> = deserialize(deserializer)?;
Self::try_from(inner, globals)
}
pub fn get_message(&self, node: &NodeMatch<StrDoc<L>>) -> String {
self.inner.get_message(node)
}
}
impl<L: Language> Deref for RuleConfig<L> {
type Target = SerializableRuleConfig<L>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::from_str;
use crate::test::TypeScript;
fn ts_rule_config(rule: SerializableRule) -> SerializableRuleConfig<TypeScript> {
let core = SerializableRuleCore {
language: TypeScript::Tsx,
rule,
constraints: None,
transform: None,
utils: None,
};
SerializableRuleConfig {
core,
id: "".into(),
message: "".into(),
note: None,
severity: Severity::Hint,
fix: None,
files: None,
ignores: None,
url: None,
metadata: None,
}
}
#[test]
fn test_rule_message() {
let globals = GlobalRules::default();
let rule = from_str("pattern: class $A {}").expect("cannot parse rule");
let mut config = ts_rule_config(rule);
config.id = "test".into();
config.message = "Found $A".into();
let grep = TypeScript::Tsx.ast_grep("class TestClass {}");
let node_match = grep
.root()
.find(config.get_matcher(&globals).unwrap())
.expect("should find match");
assert_eq!(config.get_message(&node_match), "Found TestClass");
}
#[test]
fn test_augmented_rule() {
let globals = GlobalRules::default();
let rule = from_str(
"
pattern: console.log($A)
inside:
stopBy: end
pattern: function test() { $$$ }
",
)
.expect("should parse");
let config = ts_rule_config(rule);
let grep = TypeScript::Tsx.ast_grep("console.log(1)");
let matcher = config.get_matcher(&globals).unwrap();
assert!(grep.root().find(&matcher).is_none());
let grep = TypeScript::Tsx.ast_grep("function test() { console.log(1) }");
assert!(grep.root().find(&matcher).is_some());
}
#[test]
fn test_multiple_augment_rule() {
let globals = GlobalRules::default();
let rule = from_str(
"
pattern: console.log($A)
inside:
stopBy: end
pattern: function test() { $$$ }
has:
stopBy: end
pattern: '123'
",
)
.expect("should parse");
let config = ts_rule_config(rule);
let grep = TypeScript::Tsx.ast_grep("function test() { console.log(1) }");
let matcher = config.get_matcher(&globals).unwrap();
assert!(grep.root().find(&matcher).is_none());
let grep = TypeScript::Tsx.ast_grep("function test() { console.log(123) }");
assert!(grep.root().find(&matcher).is_some());
}
#[test]
fn test_rule_env() {
let globals = GlobalRules::default();
let rule = from_str(
"
all:
- pattern: console.log($A)
- inside:
stopBy: end
pattern: function $B() {$$$}
",
)
.expect("should parse");
let config = ts_rule_config(rule);
let grep = TypeScript::Tsx.ast_grep("function test() { console.log(1) }");
let node_match = grep
.root()
.find(config.get_matcher(&globals).unwrap())
.expect("should found");
let env = node_match.get_env();
let a = env.get_match("A").expect("should exist").text();
assert_eq!(a, "1");
let b = env.get_match("B").expect("should exist").text();
assert_eq!(b, "test");
}
#[test]
fn test_transform() {
let globals = GlobalRules::default();
let rule = from_str("pattern: console.log($A)").expect("should parse");
let mut config = ts_rule_config(rule);
let transform = from_str(
"
B:
substring:
source: $A
startChar: 1
endChar: -1
",
)
.expect("should parse");
config.transform = Some(transform);
let grep = TypeScript::Tsx.ast_grep("function test() { console.log(123) }");
let node_match = grep
.root()
.find(config.get_matcher(&globals).unwrap())
.expect("should found");
let env = node_match.get_env();
let a = env.get_match("A").expect("should exist").text();
assert_eq!(a, "123");
let b = env.get_transformed("B").expect("should exist");
assert_eq!(b, b"2");
}
fn get_matches_config() -> SerializableRuleConfig<TypeScript> {
let rule = from_str(
"
matches: test-rule
",
)
.unwrap();
let utils = from_str(
"
test-rule:
pattern: some($A)
",
)
.unwrap();
let mut ret = ts_rule_config(rule);
ret.utils = Some(utils);
ret
}
#[test]
fn test_utils_rule() {
let globals = GlobalRules::default();
let config = get_matches_config();
let matcher = config.get_matcher(&globals).unwrap();
let grep = TypeScript::Tsx.ast_grep("some(123)");
assert!(grep.root().find(&matcher).is_some());
let grep = TypeScript::Tsx.ast_grep("some()");
assert!(grep.root().find(&matcher).is_none());
}
}