<commit_before>use std::collections::{HashMap, HashSet};

use crate::{
    ast::{Definition, Document, Fragment, FragmentSpread, Operation},
    parser::Spanning,
    validation::{ValidatorContext, Visitor},
    value::ScalarValue,
};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
    Operation(Option<&'a str>),
    Fragment(&'a str),
}

pub fn factory<'a>() -> NoUnusedFragments<'a> {
    NoUnusedFragments {
        spreads: HashMap::new(),
        defined_fragments: HashSet::new(),
        current_scope: None,
    }
}

pub struct NoUnusedFragments<'a> {
    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
    defined_fragments: HashSet<Spanning<&'a str>>,
    current_scope: Option<Scope<'a>>,
}

impl<'a> NoUnusedFragments<'a> {
    fn find_reachable_fragments(&'a self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
        let mut to_visit = Vec::new();
        if let Scope::Fragment(name) = *from {
            to_visit.push(name);
        }

        while let Some(from) = to_visit.pop() {
            if let Some(next) = self.find_reachable_fragments_inner(from, result) {
                to_visit.extend(next);
            }
        }
    }

    /// This function should be called only inside
    /// [`Self::find_reachable_fragments()`], as it's a recursive function using
    /// heap instead of a stack. So, instead of the recursive call, we return a
    /// [`Vec`] that is visited inside [`Self::find_reachable_fragments()`].
    fn find_reachable_fragments_inner(
        &'a self,
        from: &'a str,
        result: &mut HashSet<&'a str>,
    ) -> Option<&'a Vec<&'a str>> {
        if result.contains(from) {
            return None;
        } else {
            result.insert(from);
        }

        self.spreads.get(&Scope::Fragment(from))
    }
}

impl<'a, S> Visitor<'a, S> for NoUnusedFragments<'a>
where
    S: ScalarValue,
{
    fn exit_document(&mut self, ctx: &mut ValidatorContext<'a, S>, defs: &'a Document<S>) {
        let mut reachable = HashSet::new();

        for def in defs {
            if let Definition::Operation(Spanning {
                item: Operation { ref name, .. },
                ..
            }) = *def
            {
                let op_name = name.as_ref().map(|s| s.item);
                self.find_reachable_fragments(&Scope::Operation(op_name), &mut reachable);
            }
        }

        for fragment in &self.defined_fragments {
            if !reachable.contains(&fragment.item) {
                ctx.report_error(&error_message(fragment.item), &[fragment.start]);
            }
        }
    }

    fn enter_operation_definition(
        &mut self,
        _: &mut ValidatorContext<'a, S>,
        op: &'a Spanning<Operation<S>>,
    ) {
        let op_name = op.item.name.as_ref().map(|s| s.item);
        self.current_scope = Some(Scope::Operation(op_name));<commit_msg>Fix `NoUnusedFragments` validation rule<commit_after>use std::collections::{HashMap, HashSet};

use crate::{
    ast::{Definition, Document, Fragment, FragmentSpread, Operation},
    parser::Spanning,
    validation::{ValidatorContext, Visitor},
    value::ScalarValue,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
    Operation(Option<&'a str>),
    Fragment(&'a str),
}

pub fn factory<'a>() -> NoUnusedFragments<'a> {
    NoUnusedFragments {
        spreads: HashMap::new(),
        defined_fragments: HashSet::new(),
        current_scope: None,
    }
}

pub struct NoUnusedFragments<'a> {
    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
    defined_fragments: HashSet<Spanning<&'a str>>,
    current_scope: Option<Scope<'a>>,
}

impl<'a> NoUnusedFragments<'a> {
    fn find_reachable_fragments(&'a self, from: Scope<'a>, result: &mut HashSet<&'a str>) {
        let mut to_visit = Vec::new();
        to_visit.push(from);

        while let Some(from) = to_visit.pop() {
            if let Some(next) = self.find_reachable_fragments_inner(from, result) {
                to_visit.extend(next.iter().map(|s| Scope::Fragment(s)));
            }
        }
    }

    /// This function should be called only inside
    /// [`Self::find_reachable_fragments()`], as it's a recursive function using
    /// heap instead of a stack. So, instead of the recursive call, we return a
    /// [`Vec`] that is visited inside [`Self::find_reachable_fragments()`].
    fn find_reachable_fragments_inner(
        &'a self,
        from: Scope<'a>,
        result: &mut HashSet<&'a str>,
    ) -> Option<&'a Vec<&'a str>> {
        if let Scope::Fragment(name) = from {
            if result.contains(name) {
                return None;
            } else {
                result.insert(name);
            }
        }

        self.spreads.get(&from)
    }
}

impl<'a, S> Visitor<'a, S> for NoUnusedFragments<'a>
where
    S: ScalarValue,
{
    fn exit_document(&mut self, ctx: &mut ValidatorContext<'a, S>, defs: &'a Document<S>) {
        let mut reachable = HashSet::new();

        for def in defs {
            if let Definition::Operation(Spanning {
                item: Operation { ref name, .. },
                ..
            }) = *def
            {
                let op_name = name.as_ref().map(|s| s.item);
                self.find_reachable_fragments(Scope::Operation(op_name), &mut reachable);
            }
        }

        for fragment in &self.defined_fragments {
            if !reachable.contains(&fragment.item) {
                ctx.report_error(&error_message(fragment.item), &[fragment.start]);
            }
        }
    }

    fn enter_operation_definition(
        &mut self,
        _: &mut ValidatorContext<'a, S>,
        op: &'a Spanning<Operation<S>>,
    ) {
        let op_name = op.item.name.as_ref().map(|s| s.item);
        self.current_scope = Some(Scope::Operation(op_name));
{'fraction_non_alphanumeric': 0.14742817822122828, 'fraction_numerical': 0.0, 'mean_word_length': 2.3980128579777906, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 62, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7481705', 'n_tokens_mistral': 1877, 'n_tokens_neox': 1873, 'n_words': 403}