function_grep/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(clippy::unwrap_used, clippy::expect_used)]
3#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
4#![deny(missing_debug_implementations, clippy::missing_panics_doc)]
5#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
6#![deny(clippy::use_self, rust_2018_idioms)]
7use core::fmt;
8
9use supported_languages::SupportedLanguage;
10use tree_sitter::{Language, LanguageError, Node, QueryError, Range, Tree};
11
12/// For adding new language support, and some predefined support for certain languages,
13pub mod supported_languages;
14
15fn run_query<'a>(
16    query_str: &'a str,
17    lang: Language,
18    node: Node<'a>,
19    code: &'a [u8],
20) -> Result<Box<[Range]>, QueryError> {
21    let query = tree_sitter::Query::new(lang, query_str)?;
22    let mut query_cursor = tree_sitter::QueryCursor::new();
23    let matches = query_cursor.matches(&query, node, code);
24    let ranges = matches.map(|m| m.captures[0].node.range());
25    Ok(ranges.collect())
26}
27
28/// Errors that we may give back.
29#[derive(Debug)]
30pub enum Error {
31    /// If there is no language that has this file extension or the file does not have a file
32    /// extension.
33    FileTypeUnkown(String),
34    /// If tree sitter fails to parse the file.
35    ParseError(String),
36    /// If tree sitter doesn't like the grammer for given language
37    GrammarLoad(&'static str, LanguageError),
38    /// If tree sitter doesn't like the query from a given [SupportedLanguage].
39    InvalidQuery(&'static str, QueryError),
40    /// If there are no result after filtering.
41    NoSuchResultsForFilter,
42    /// If there are no result after searching.
43    NoResultsForSearch,
44}
45
46/// Tries to find the appropiate language for the given file extension [`ext`] based on the list of
47/// languages [`langs`] provided.
48///
49/// # Errors
50/// If there is no language for this file extension in the provided language list.
51pub fn get_file_type_from_file_ext<'a>(
52    ext: &str,
53    langs: &'a [&'a dyn SupportedLanguage],
54) -> Result<&'a dyn SupportedLanguage, Error> {
55    langs
56        .iter()
57        .find(|lang| lang.file_exts().contains(&ext))
58        .copied()
59        .ok_or_else(|| Error::FileTypeUnkown(ext.to_string()))
60}
61
62/// Tries to find the appropiate language for the given file [`file_name`] based on the list of
63/// languages [`langs`] provided.
64/// This works by obtaining the extension from the file path and using
65/// [`get_file_type_from_file_ext`].
66///
67/// # Errors
68/// If there is no file extension for this file name, or there is no language for this file in the provided language list.
69pub fn get_file_type_from_file<'a>(
70    file_name: &str,
71    langs: &'a [&'a dyn SupportedLanguage],
72) -> Result<&'a dyn SupportedLanguage, Error> {
73    file_name
74        .rsplit_once('.')
75        .ok_or_else(|| Error::FileTypeUnkown(file_name.to_string()))
76        .map(|(_, ext)| ext)
77        .and_then(|ext| get_file_type_from_file_ext(ext, langs))
78}
79
80#[derive(Debug, Clone)]
81/// The result of finding function with a given name.
82/// Use [`Self::search_file`] or [`Self::search_file_with_name`] to do the searching.
83pub struct ParsedFile<'a> {
84    // I believe we cannot store something refernceing the tree, so we cannot directly store the
85    // results of the query, but just their ranges so in the [`filter`] method we use the tree to
86    // obtain the correct nodes from their ranges
87    file: &'a str,
88    file_name: Option<&'a str>,
89    function_name: &'a str,
90    // TODO: maybe each supported language could define filters
91    // if so we would store dyn SupportedLanguage here
92    language_type: &'a str,
93    tree: Tree,
94    results: Box<[Range]>,
95}
96
97impl<'a> ParsedFile<'a> {
98    #[must_use]
99    pub fn new(
100        file: &'a str,
101        function_name: &'a str,
102        language_type: &'a str,
103        tree: Tree,
104        results: Box<[Range]>,
105    ) -> Self {
106        Self {
107            file,
108            function_name,
109            language_type,
110            tree,
111            results,
112            file_name: None,
113        }
114    }
115
116    // TODO: maybe only make this hidden and expose a filter method that takes in some sort of
117    // filter trait
118    //
119    /// Filters out commits not matching the filter [`f`].
120    /// Returns new version of the current [`ParsedFile`] with only the results that match the
121    /// filter.
122    ///
123    /// # Errors
124    /// If the filter [`f`] filters out all the results of this file
125    pub fn filter(&self, f: fn(&Node<'_>) -> bool) -> Result<Self, Error> {
126        let root = self.tree.root_node();
127        let ranges: Box<[Range]> = self
128            .ranges()
129            .filter_map(|range| root.descendant_for_point_range(range.start_point, range.end_point))
130            .filter(f)
131            .map(|n| n.range())
132            .collect();
133        if ranges.is_empty() {
134            return Err(Error::NoSuchResultsForFilter);
135        }
136        let clone = Self {
137            results: ranges,
138            ..self.clone()
139        };
140        Ok(clone)
141    }
142
143    #[must_use]
144    /// Get the name of the language used to parse this file
145    pub const fn language(&self) -> &str {
146        self.language_type
147    }
148
149    #[must_use]
150    /// Get the name of the function that was searched for to make this [`ParsedFile`]
151    pub const fn search_name(&self) -> &str {
152        self.function_name
153    }
154
155    fn ranges(&self) -> impl Iterator<Item = &Range> {
156        self.results.iter()
157    }
158    /// Search for all function with the name [`name`], in string [`code`] with the specified
159    /// language [`language`].
160    ///
161    /// Note: to obtain the the language you may use [`get_file_type_from_file`] or
162    /// [`get_file_type_from_file_ext`].
163    /// Alternativly use [`Self::search_file_with_name`] to let us find the correct language for you.
164    ///
165    /// # Errors
166    /// If something with tree sitter goes wrong.
167    /// If the code cannot be parsed properly.
168    /// If no results are found for this function name.
169    pub fn search_file(
170        name: &'a str,
171        code: &'a str,
172        language: &'a dyn SupportedLanguage,
173    ) -> Result<Self, Error> {
174        let code_bytes = code.as_bytes();
175        let mut parser = tree_sitter::Parser::new();
176        let ts_lang = language.language();
177        parser
178            .set_language(ts_lang)
179            .map_err(|lang_err| Error::GrammarLoad(language.name(), lang_err))?;
180        let parsed = parser
181            .parse(code, None)
182            .ok_or_else(|| Error::ParseError(code.to_string()))?;
183
184        let query_str = language.query(name);
185        let node = parsed.root_node();
186        let command_ranges = run_query(&query_str, ts_lang, node, code_bytes)
187            .map_err(|query_err| Error::InvalidQuery(language.name(), query_err))?;
188
189        if command_ranges.is_empty() {
190            return Err(Error::NoResultsForSearch);
191        }
192        Ok(ParsedFile::new(
193            code,
194            name,
195            language.name(),
196            parsed,
197            command_ranges,
198        ))
199    }
200
201    /// Search for all function with the name [`name`], in string [`code`] with a language found
202    /// from the file name [`file_name`] and the languages [`langs`].
203    ///
204    /// # Errors
205    /// If there is no language found for the given file name.
206    /// If something with tree sitter goes wrong.
207    /// If the code cannot be parsed properly,
208    /// If no results are found for this function name.
209    pub fn search_file_with_name(
210        name: &'a str,
211        code: &'a str,
212        file_name: &'a str,
213        langs: &'a [&'a dyn SupportedLanguage],
214    ) -> Result<Self, Error> {
215        get_file_type_from_file(file_name, langs)
216            .and_then(|language| Self::search_file(name, code, language))
217            .map(|file| file.set_file_name(file_name))
218    }
219
220    fn set_file_name(mut self, file_name: &'a str) -> Self {
221        self.file_name.replace(file_name);
222        self
223    }
224
225    #[must_use]
226    /// Get the file name of this file.
227    pub const fn file_name(&self) -> Option<&str> {
228        self.file_name
229    }
230
231    #[must_use]
232    /// Get the [Range] of each found function.
233    pub fn results(&self) -> &[Range] {
234        &self.results
235    }
236}
237
238impl IntoIterator for ParsedFile<'_> {
239    type Item = (Range, String);
240
241    type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
242
243    fn into_iter(self) -> Self::IntoIter {
244        Box::new(
245            self.ranges()
246                .map(|range| {
247                    (
248                        *range,
249                        self.file[range.start_byte..range.end_byte].to_string(),
250                    )
251                })
252                .collect::<Vec<_>>()
253                .into_iter(),
254        )
255    }
256}
257
258impl fmt::Display for ParsedFile<'_> {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        let lines = self.file.lines().enumerate();
261        let texts = self
262            .ranges()
263            .map(move |range| {
264                lines
265                    .clone()
266                    .filter_map(move |(line, str)| {
267                        if line >= range.start_point.row && line <= range.end_point.row {
268                            Some(format!("{}: {str}", line + 1))
269                        } else {
270                            None
271                        }
272                    })
273                    .collect::<Vec<_>>()
274                    .join("\n")
275            })
276            .collect::<Vec<_>>()
277            .join("\n...\n");
278        write!(f, "{texts}")
279    }
280}