use hashbrown::HashMap;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::lint_fix::LintFix;
use sqruff_lib_core::parser::segments::SegmentBuilder;
use crate::core::config::Value;
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
#[derive(Debug, Clone)]
pub struct RuleAM08;
impl Rule for RuleAM08 {
fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
Ok(RuleAM08.erased())
}
fn name(&self) -> &'static str {
"ambiguous.join_condition"
}
fn description(&self) -> &'static str {
"Implicit cross join detected."
}
fn long_description(&self) -> &'static str {
r#"
**Anti-pattern**
Cross joins are valid, but rare in the wild - and more often created by mistake than on purpose. This rule catches situations where a cross join has been specified, but not explicitly and so the risk of a mistaken cross join is highly likely.
```sql
SELECT
foo
FROM bar
JOIN baz;
```
**Best practice**
Use `CROSS JOIN`.
```sql
SELECT
foo
FROM bar
CROSS JOIN baz;
```
"#
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::Ambiguous]
}
fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
assert!(context.segment.is_type(SyntaxKind::JoinClause));
let children = context.segment.segments();
let has_condition = children.iter().any(|s| {
s.is_type(SyntaxKind::JoinOnCondition)
|| s.is_type(SyntaxKind::UsingClause)
|| s.is_keyword("USING")
});
if has_condition {
return Vec::new();
}
let keywords: Vec<_> = children
.iter()
.filter(|s| s.is_type(SyntaxKind::Keyword))
.collect();
if keywords.iter().any(|k| {
k.raw().eq_ignore_ascii_case("CROSS") || k.raw().eq_ignore_ascii_case("NATURAL")
}) {
return Vec::new();
}
let join_keyword = keywords
.iter()
.find(|k| k.raw().eq_ignore_ascii_case("JOIN"));
let Some(join_kw) = join_keyword else {
return Vec::new();
};
let cross_keyword = if join_kw.raw() == "JOIN" || join_kw.raw() == "Join" {
"CROSS"
} else {
"cross"
};
vec![LintResult::new(
Some((*join_kw).clone()),
vec![LintFix::create_before(
(*join_kw).clone(),
vec![
SegmentBuilder::keyword(context.tables.next_id(), cross_keyword),
SegmentBuilder::whitespace(context.tables.next_id(), " "),
],
)],
None,
None,
)]
}
fn is_fix_compatible(&self) -> bool {
true
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::JoinClause]) }).into()
}
}