use std::collections::BTreeSet;
use std::num::NonZeroUsize;
use std::sync::Mutex;
use clippy_utils::diagnostics::span_lint_and_sugg;
use rustc_ast::{LitKind, StrStyle};
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass, LintContext, LintStore};
use rustc_session::{declare_tool_lint, impl_lint_pass};
mod early;
mod emit;
mod parser;
mod queue;
use early::PreferRawStringEarly;
use parser::{
DEFAULT_ELIGIBLE_ESCAPES, build_raw_string_suggestion, is_supported_eligible_entry, scan_body,
};
use queue::PendingViolation;
use crate::common::{DefaultState, resolved_state};
use crate::enclosing_hir::find_enclosing_hir_ids;
declare_tool_lint! {
pub perfectionist::PREFER_RAW_STRING,
Warn,
"string literal contains only raw-expressible escapes; prefer the raw-string form",
report_in_external_macro: true
}
const CONFIG_KEY: &str = "perfectionist::prefer_raw_string";
pub(super) const VIOLATION_MESSAGE: &str =
"string literal uses escapes that a raw string would avoid";
pub(super) const SUGGESTION_LABEL: &str = "use a raw string";
#[derive(Debug, serde::Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "snake_case")]
struct Config {
min_escapes_to_trigger: NonZeroUsize,
eligible_escapes: Vec<String>,
}
const DEFAULT_MIN_ESCAPES_TO_TRIGGER: NonZeroUsize = NonZeroUsize::new(1).expect("1 is non-zero");
impl Default for Config {
fn default() -> Self {
Self {
min_escapes_to_trigger: DEFAULT_MIN_ESCAPES_TO_TRIGGER,
eligible_escapes: DEFAULT_ELIGIBLE_ESCAPES
.iter()
.map(|entry| (*entry).to_owned())
.collect(),
}
}
}
pub(super) struct ResolvedConfig {
pub(super) min_escapes_to_trigger: NonZeroUsize,
pub(super) eligible_escapes: Vec<String>,
}
pub(super) fn resolved_config() -> ResolvedConfig {
let config: Config = dylint_linting::config_or_default(CONFIG_KEY);
let eligible_escapes = config
.eligible_escapes
.into_iter()
.filter(|entry| is_supported_eligible_entry(entry))
.collect();
ResolvedConfig {
min_escapes_to_trigger: config.min_escapes_to_trigger,
eligible_escapes,
}
}
pub struct PreferRawString {
min_escapes_to_trigger: NonZeroUsize,
eligible_escapes: Vec<String>,
}
impl PreferRawString {
fn new() -> Self {
let resolved = resolved_config();
Self {
min_escapes_to_trigger: resolved.min_escapes_to_trigger,
eligible_escapes: resolved.eligible_escapes,
}
}
}
static PENDING_VIOLATIONS: Mutex<Vec<PendingViolation>> = Mutex::new(Vec::new());
static VISITED_LITERALS: Mutex<BTreeSet<(u32, u32)>> = Mutex::new(BTreeSet::new());
fn queue(violation: PendingViolation) {
let mut guard = PENDING_VIOLATIONS
.lock()
.unwrap_or_else(|err| err.into_inner());
guard.push(violation);
}
impl_lint_pass!(PreferRawString => [PREFER_RAW_STRING]);
impl_lint_pass!(PreferRawStringEarly => [PREFER_RAW_STRING]);
pub fn register_lint(lint_store: &mut LintStore) {
lint_store.register_lints(&[PREFER_RAW_STRING]);
}
pub fn register_pass(lint_store: &mut LintStore) {
if let DefaultState::Inactive = resolved_state("prefer_raw_string", DefaultState::Active) {
return;
}
lint_store.register_pre_expansion_pass(|| Box::new(PreferRawStringEarly::new()));
lint_store.register_late_pass(|_| Box::new(PreferRawString::new()));
}
impl<'tcx> LateLintPass<'tcx> for PreferRawString {
fn check_expr(&mut self, lint_context: &LateContext<'tcx>, expr: &Expr<'tcx>) {
let ExprKind::Lit(literal) = expr.kind else {
return;
};
if !matches!(literal.node, LitKind::Str(_, StrStyle::Cooked)) {
return;
}
if !VISITED_LITERALS
.lock()
.unwrap_or_else(|err| err.into_inner())
.insert((literal.span.lo().0, literal.span.hi().0))
{
return;
}
let Ok(snippet) = lint_context
.sess()
.source_map()
.span_to_snippet(literal.span)
else {
return;
};
let Some(body) = snippet
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
else {
return;
};
let Some(scan) = scan_body(body, &self.eligible_escapes) else {
return;
};
if scan.eliminable_count < self.min_escapes_to_trigger.get() {
return;
}
span_lint_and_sugg(
lint_context,
PREFER_RAW_STRING,
literal.span,
VIOLATION_MESSAGE,
SUGGESTION_LABEL,
build_raw_string_suggestion(&scan.decoded),
Applicability::MachineApplicable,
);
}
fn check_crate_post(&mut self, lint_context: &LateContext<'tcx>) {
let pending: Vec<PendingViolation> = {
let mut guard = PENDING_VIOLATIONS
.lock()
.unwrap_or_else(|err| err.into_inner());
std::mem::take(&mut *guard)
};
let visited: BTreeSet<(u32, u32)> = {
let mut guard = VISITED_LITERALS
.lock()
.unwrap_or_else(|err| err.into_inner());
std::mem::take(&mut *guard)
};
let mut emitted: BTreeSet<(u32, u32)> = BTreeSet::new();
let surviving: Vec<PendingViolation> = pending
.into_iter()
.filter(|violation| {
let range = (violation.span.lo().0, violation.span.hi().0);
!visited.contains(&range) && emitted.insert(range)
})
.collect();
if surviving.is_empty() {
return;
}
let target_spans: Vec<_> = surviving.iter().map(|violation| violation.span).collect();
let best = find_enclosing_hir_ids(lint_context.tcx, &target_spans);
for (violation, &hir_id) in surviving.into_iter().zip(best.iter()) {
emit::emit_raw_string(lint_context, hir_id, violation.span, violation.suggestion);
}
}
}