fichu 0.1.9

A formatter for SPARQL queries
Documentation
mod state;

use std::collections::HashSet;

use log::{error, info};
pub use state::*;

use tree_sitter::{Node, Query, QueryCursor};

use crate::lsp::textdocument::{Position, Range};

fn collect_all_unique_captures(node: Node, query_str: &str, text: &String) -> Vec<String> {
    match Query::new(&tree_sitter_sparql::language(), query_str) {
        Ok(query) => QueryCursor::new()
            .captures(&query, node, text.as_bytes())
            .map(|(query_match, capture_index)| {
                query_match.captures[capture_index]
                    .node
                    .utf8_text(text.as_bytes())
                    .unwrap()
                    .split_at(1)
                    .1
                    .to_string()
            })
            .collect::<HashSet<String>>()
            .into_iter()
            .collect(),

        Err(_) => {
            error!("Building a tree-sitter query failed: {}", query_str);
            vec![]
        }
    }
}

pub fn get_all_variables(analyis_state: &AnalysisState, uri: &String) -> Vec<String> {
    match analyis_state.get_state(uri) {
        Some((document, Some(tree))) => {
            collect_all_unique_captures(tree.root_node(), "(VAR) @variable", &document.text)
        }
        Some((_document, None)) => {
            info!("Could not compute variables for {}: No tree availible", uri);
            vec![]
        }
        None => {
            error!("Could not compute variables for {}: No such document", uri);
            vec![]
        }
    }
}

pub fn get_kind_at_position(
    analyis_state: &AnalysisState,
    uri: &String,
    position: &Position,
) -> Option<&'static str> {
    match analyis_state.get_tree(uri) {
        Some(tree) => {
            let point = position.to_point();
            Some(
                tree.root_node()
                    .descendant_for_point_range(point, point)?
                    .kind(),
            )
        }
        None => None,
    }
}

pub fn get_declared_namspaces(analyis_state: &AnalysisState, uri: &String) -> Vec<(String, Range)> {
    match analyis_state.get_state(uri) {
        Some((document, Some(tree))) => {
            match Query::new(
                &tree_sitter_sparql::language(),
                "(PrefixDecl (PNAME_NS) @namespace)",
            ) {
                Ok(query) => QueryCursor::new()
                    .captures(&query, tree.root_node(), document.text.as_bytes())
                    .map(|(query_match, capture_index)| {
                        let node = query_match.captures[capture_index].node;
                        (
                            node.utf8_text(document.text.as_bytes())
                                .unwrap()
                                .to_string(),
                            Range::from_node(node),
                        )
                    })
                    .collect(),

                Err(_) => {
                    error!(
                        "Could not compute declared namspaces for {}: Query could not be build",
                        uri
                    );
                    vec![]
                }
            }
        }
        Some((_document, None)) => {
            info!(
                "Could not compute declared namespaces for {}: No tree availible",
                uri
            );
            vec![]
        }
        None => {
            error!(
                "Could not compute declared namspaces for {}: No such document",
                uri
            );
            vec![]
        }
    }
}

pub fn get_used_namspaces(analyis_state: &AnalysisState, uri: &String) -> Vec<(String, Range)> {
    match analyis_state.get_state(uri) {
        Some((document, Some(tree))) => {
            match Query::new(
                &tree_sitter_sparql::language(),
                "(PrefixedName (PNAME_NS) @namespace)",
            ) {
                Ok(query) => QueryCursor::new()
                    .captures(&query, tree.root_node(), document.text.as_bytes())
                    .map(|(query_match, capture_index)| {
                        let node = query_match.captures[capture_index].node;
                        (
                            node.utf8_text(document.text.as_bytes())
                                .unwrap()
                                .to_string(),
                            Range::from_node(node),
                        )
                    })
                    .collect(),

                Err(_) => {
                    error!(
                        "Could not compute declared namspaces for {}: Query could not be build",
                        uri
                    );
                    vec![]
                }
            }
        }
        Some((_document, None)) => {
            info!(
                "Could not compute declared namespaces for {}: No tree availible",
                uri
            );
            vec![]
        }
        None => {
            error!(
                "Could not compute declared namspaces for {}: No such document",
                uri
            );
            vec![]
        }
    }
}

pub(crate) fn get_unused_prefixes(
    analysis_state: &AnalysisState,
    uri: &String,
) -> impl Iterator<Item = (String, Range)> {
    let declared_namespaces = get_declared_namspaces(analysis_state, uri);
    let declared_namespaces_set: HashSet<String> = declared_namespaces
        .iter()
        .map(|(namespace, _range)| namespace)
        .cloned()
        .collect();
    let used_namespaces = get_used_namspaces(analysis_state, uri);
    let used_namespaces_set: HashSet<String> = used_namespaces
        .iter()
        .map(|(namespace, _range)| namespace)
        .cloned()
        .collect();

    declared_namespaces
        .iter()
        .filter_map(move |(declared_namespace, range)| {
            (&declared_namespaces_set - &used_namespaces_set)
                .contains(declared_namespace)
                .then_some((declared_namespace.clone(), range.clone()))
        })
        .collect::<Vec<(String, Range)>>()
        .into_iter()
}

pub(crate) fn get_undeclared_prefixes(
    analysis_state: &AnalysisState,
    uri: &String,
) -> impl Iterator<Item = (String, Range)> {
    let declared_namespaces = get_declared_namspaces(analysis_state, uri);
    let declared_namespaces_set: HashSet<String> = declared_namespaces
        .iter()
        .map(|(namespace, _range)| namespace)
        .cloned()
        .collect();
    let used_namespaces = get_used_namspaces(analysis_state, uri);
    let used_namespaces_set: HashSet<String> = used_namespaces
        .iter()
        .map(|(namespace, _range)| namespace)
        .cloned()
        .collect();

    used_namespaces
        .iter()
        .filter_map(|(declared_namespace, range)| {
            (&used_namespaces_set - &declared_namespaces_set)
                .contains(declared_namespace)
                .then_some((declared_namespace.clone(), range.clone()))
        })
        .collect::<Vec<(String, Range)>>()
        .into_iter()
}

#[cfg(test)]
mod tests {
    use indoc::indoc;

    use crate::{
        analysis::{
            get_declared_namspaces, get_undeclared_prefixes, get_unused_prefixes,
            get_used_namspaces, AnalysisState,
        },
        lsp::textdocument::TextDocumentItem,
    };

    #[test]
    fn declared_namespaces() {
        let mut state = AnalysisState::new();
        state.add_document(TextDocumentItem::new(
            "uri",
            indoc!(
                "PREFIX wdt: <iri>
                 PREFIX wd: <iri>
                 PREFIX wdt: <iri>

                 SELECT * {}"
            ),
        ));
        let declared_namesapces = get_declared_namspaces(&state, &"uri".to_string());
        assert_eq!(
            declared_namesapces
                .iter()
                .map(|(namespace, _range)| namespace)
                .collect::<Vec<&String>>(),
            vec!["wdt:", "wd:", "wdt:"]
        );
    }

    #[test]
    fn used_namespaces() {
        let mut state = AnalysisState::new();
        state.add_document(TextDocumentItem::new(
            "uri",
            indoc!("SELECT * {?a wdt:P32 ?b. ?a wd:p32 ?b. ?a wdt:P31 ?b}"),
        ));
        let declared_namesapces = get_used_namspaces(&state, &"uri".to_string());
        assert_eq!(
            declared_namesapces
                .iter()
                .map(|(namespace, _range)| namespace)
                .collect::<Vec<&String>>(),
            vec!["wdt:", "wd:", "wdt:"]
        );
    }

    #[test]
    fn undeclared_namespaces() {
        let mut state = AnalysisState::new();
        state.add_document(TextDocumentItem::new(
            "uri",
            indoc!("SELECT * {x:y y:p x:x}"),
        ));
        let declared_namesapces: Vec<String> = get_undeclared_prefixes(&state, &"uri".to_string())
            .map(|(namespace, _range)| namespace)
            .collect();
        assert_eq!(declared_namesapces, vec!["x:", "y:", "x:"]);
    }
    #[test]
    fn unused_namespaces() {
        let mut state = AnalysisState::new();
        state.add_document(TextDocumentItem::new(
            "uri",
            indoc!(
                "PREFIX wdt: <>
                 PREFIX wdt: <>

                 SELECT * {}"
            ),
        ));
        let declared_namesapces: Vec<String> = get_unused_prefixes(&state, &"uri".to_string())
            .map(|(namespace, _range)| namespace)
            .collect();
        assert_eq!(declared_namesapces, vec!["wdt:", "wdt:"]);
    }
}