use regex::Regex;
use crate::core::config::Value;
use crate::core::dialects::init::DialectKind;
use crate::core::parser::parsers::RegexParser;
use crate::core::parser::segments::base::SegmentBuilder;
use crate::core::rules::base::{Erased, ErasedRule, LintFix, LintResult, Rule, RuleGroups};
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
use crate::dialects::{SyntaxKind, SyntaxSet};
use crate::utils::functional::context::FunctionalContext;
#[derive(Default, Debug, Clone)]
pub struct RuleRF06 {
prefer_quoted_identifiers: bool,
prefer_quoted_keywords: bool,
ignore_words: Vec<String>,
ignore_words_regex: Vec<Regex>,
force_enable: bool,
}
impl Rule for RuleRF06 {
fn load_from_config(
&self,
config: &ahash::AHashMap<String, Value>,
) -> Result<ErasedRule, String> {
Ok(Self {
prefer_quoted_identifiers: config["prefer_quoted_identifiers"].as_bool().unwrap(),
prefer_quoted_keywords: config["prefer_quoted_keywords"].as_bool().unwrap(),
ignore_words: config["ignore_words"]
.map(|it| {
it.as_array()
.unwrap()
.iter()
.map(|it| it.as_string().unwrap().to_lowercase())
.collect()
})
.unwrap_or_default(),
ignore_words_regex: config["ignore_words_regex"]
.map(|it| {
it.as_array()
.unwrap()
.iter()
.map(|it| Regex::new(it.as_string().unwrap()).unwrap())
.collect()
})
.unwrap_or_default(),
force_enable: config["force_enable"].as_bool().unwrap(),
}
.erased())
}
fn name(&self) -> &'static str {
"references.quoting"
}
fn description(&self) -> &'static str {
"Unnecessary quoted identifier."
}
fn long_description(&self) -> &'static str {
r#"
**Anti-pattern**
In this example, a valid unquoted identifier, that is also not a reserved keyword, is needlessly quoted.
```sql
SELECT 123 as "foo"
```
**Best practice**
Use unquoted identifiers where possible.
```sql
SELECT 123 as foo
```
When `prefer_quoted_identifiers = True`, the quotes are always necessary, no matter if the identifier is valid, a reserved keyword, or contains special characters.
> **Note**
> Note due to different quotes being used by different dialects supported by `SQLFluff`, and those quotes meaning different things in different contexts, this mode is not `sqlfluff fix` compatible.
**Anti-pattern**
In this example, a valid unquoted identifier, that is also not a reserved keyword, is required to be quoted.
```sql
SELECT 123 as foo
```
**Best practice**
Use quoted identifiers.
```sql
SELECT 123 as "foo" -- For ANSI, ...
-- or
SELECT 123 as `foo` -- For BigQuery, MySql, ...
```"#
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::References]
}
fn eval(&self, context: RuleContext) -> Vec<LintResult> {
if matches!(context.dialect.name, DialectKind::Postgres | DialectKind::Snowflake)
&& !self.force_enable
{
return Vec::new();
}
if FunctionalContext::new(context.clone()).parent_stack().any(Some(|it| {
[SyntaxKind::PasswordAuth, SyntaxKind::ExecuteAsClause]
.into_iter()
.any(|ty| it.is_type(ty))
})) {
return Vec::new();
}
let identifier_is_quoted =
!lazy_regex::regex_is_match!(r#"^[^"\'\[].+[^"\'\]]$"#, context.segment.raw().as_ref());
let identifier_contents = context.segment.raw();
let identifier_contents = if identifier_is_quoted {
identifier_contents
.get(1..identifier_contents.len() - 1)
.map(ToOwned::to_owned)
.unwrap_or_default()
} else {
identifier_contents.to_string()
};
let identifier_is_keyword = context
.dialect
.sets("reserved_keywords")
.contains(identifier_contents.to_uppercase().as_str())
|| context
.dialect
.sets("unreserved_keywords")
.contains(identifier_contents.to_uppercase().as_str());
let context_policy = if self.prefer_quoted_identifiers {
SyntaxKind::NakedIdentifier
} else {
SyntaxKind::QuotedIdentifier
};
if self.ignore_words.contains(&identifier_contents.to_lowercase()) {
return Vec::new();
}
if self.ignore_words_regex.iter().any(|regex| regex.is_match(identifier_contents.as_ref()))
{
return Vec::new();
}
if self.prefer_quoted_keywords && identifier_is_keyword {
return if !identifier_is_quoted {
vec![LintResult::new(
context.segment.into(),
Vec::new(),
None,
Some(format!("Missing quoted keyword identifier {identifier_contents}.")),
None,
)]
} else {
Vec::new()
};
}
if !context.segment.is_type(context_policy)
|| context.segment.raw().eq_ignore_ascii_case("quoted_identifier")
|| context.segment.raw().eq_ignore_ascii_case("naked_identifier")
{
return Vec::new();
}
if self.prefer_quoted_identifiers {
return vec![LintResult::new(
context.segment.into(),
Vec::new(),
None,
Some(format!("Missing quoted identifier {identifier_contents}.")),
None,
)];
}
let owned = context.dialect.grammar("NakedIdentifierSegment");
let naked_identifier_parser = owned.as_any().downcast_ref::<RegexParser>().unwrap();
if is_full_match(naked_identifier_parser.template.as_str(), &identifier_contents)
&& naked_identifier_parser.anti_template.as_ref().map_or(true, |anti_template| {
!is_full_match(anti_template.as_str(), &identifier_contents)
})
{
return vec![LintResult::new(
context.segment.clone().into(),
vec![LintFix::replace(
context.segment.clone(),
vec![
SegmentBuilder::token(
context.tables.next_id(),
&identifier_contents,
SyntaxKind::NakedIdentifier,
)
.finish(),
],
None,
)],
None,
Some(format!("Unnecessary quoted identifier {}.", context.segment.raw())),
None,
)];
}
Vec::new()
}
fn is_fix_compatible(&self) -> bool {
true
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(
const { SyntaxSet::new(&[SyntaxKind::QuotedIdentifier, SyntaxKind::NakedIdentifier]) },
)
.into()
}
}
fn is_full_match(pattern: &str, text: &str) -> bool {
let full_pattern = format!("(?i)^{}$", pattern); let regex = fancy_regex::Regex::new(&full_pattern).unwrap();
regex.is_match(text).unwrap()
}