libgir/codegen/doc/
format.rs

1#![allow(clippy::manual_map)]
2use std::{fmt::Write, sync::OnceLock};
3
4use log::{info, warn};
5use regex::{Captures, Regex};
6
7use super::{gi_docgen, LocationInObject};
8use crate::{
9    analysis::functions::Info,
10    library::{FunctionKind, TypeId},
11    nameutil, Env,
12};
13
14const LANGUAGE_SEP_BEGIN: &str = "<!-- language=\"";
15const LANGUAGE_SEP_END: &str = "\" -->";
16const LANGUAGE_BLOCK_BEGIN: &str = "|[";
17const LANGUAGE_BLOCK_END: &str = "\n]|";
18
19// A list of function names that are ignored when warning about a "not found
20// function"
21const IGNORE_C_WARNING_FUNCS: [&str; 6] = [
22    "g_object_unref",
23    "g_object_ref",
24    "g_free",
25    "g_list_free",
26    "g_strfreev",
27    "printf",
28];
29
30pub fn reformat_doc(
31    input: &str,
32    env: &Env,
33    in_type: Option<(&TypeId, Option<LocationInObject>)>,
34) -> String {
35    code_blocks_transformation(input, env, in_type)
36}
37
38fn try_split<'a>(src: &'a str, needle: &str) -> (&'a str, Option<&'a str>) {
39    match src.find(needle) {
40        Some(pos) => (&src[..pos], Some(&src[pos + needle.len()..])),
41        None => (src, None),
42    }
43}
44
45fn code_blocks_transformation(
46    mut input: &str,
47    env: &Env,
48    in_type: Option<(&TypeId, Option<LocationInObject>)>,
49) -> String {
50    let mut out = String::with_capacity(input.len());
51
52    loop {
53        input = match try_split(input, LANGUAGE_BLOCK_BEGIN) {
54            (before, Some(after)) => {
55                out.push_str(&format(before, env, in_type));
56                if let (before, Some(after)) =
57                    try_split(get_language(after, &mut out), LANGUAGE_BLOCK_END)
58                {
59                    out.push_str(before);
60                    out.push_str("\n```");
61                    after
62                } else {
63                    after
64                }
65            }
66            (before, None) => {
67                out.push_str(&format(before, env, in_type));
68                return out;
69            }
70        };
71    }
72}
73
74fn get_language<'a>(entry: &'a str, out: &mut String) -> &'a str {
75    if let (_, Some(after)) = try_split(entry, LANGUAGE_SEP_BEGIN) {
76        if let (before, Some(after)) = try_split(after, LANGUAGE_SEP_END) {
77            if !["text", "rust"].contains(&before) {
78                write!(out, "\n\n**⚠️ The following code is in {before} ⚠️**").unwrap();
79            }
80            write!(out, "\n\n```{before}").unwrap();
81            return after;
82        }
83    }
84    out.push_str("\n```text");
85    entry
86}
87
88// try to get the language if any is defined or fallback to text
89fn get_markdown_language(input: &str) -> (&str, &str) {
90    let (lang, after) = if let Some((lang, after)) = input.split_once('\n') {
91        let lang = if lang.is_empty() { None } else { Some(lang) };
92        (lang, after)
93    } else {
94        (None, input)
95    };
96    (lang.unwrap_or("text"), after)
97}
98
99// Re-format codeblocks & replaces the C types and GI-docgen with proper links
100fn format(
101    mut input: &str,
102    env: &Env,
103    in_type: Option<(&TypeId, Option<LocationInObject>)>,
104) -> String {
105    let mut ret = String::with_capacity(input.len());
106    loop {
107        input = match try_split(input, "```") {
108            (before, Some(after)) => {
109                // if we are inside a codeblock
110                ret.push_str(&replace_symbols(before, env, in_type));
111
112                let (lang, after) = get_markdown_language(after);
113                if !["text", "rust", "xml", "css", "json", "html"].contains(&lang)
114                    && after.lines().count() > 1
115                {
116                    write!(ret, "**⚠️ The following code is in {lang} ⚠️**\n\n").unwrap();
117                }
118                writeln!(ret, "```{lang}").unwrap();
119
120                if let (before, Some(after)) = try_split(after, "```") {
121                    ret.push_str(before);
122                    ret.push_str("```");
123                    after
124                } else {
125                    after
126                }
127            }
128            (before, None) => {
129                ret.push_str(&replace_symbols(before, env, in_type));
130                return ret;
131            }
132        }
133    }
134}
135
136fn replace_symbols(
137    input: &str,
138    env: &Env,
139    in_type: Option<(&TypeId, Option<LocationInObject>)>,
140) -> String {
141    if env.config.use_gi_docgen {
142        let out = gi_docgen::replace_c_types(input, env, in_type);
143        let out = gi_docgen_symbol().replace_all(&out, |caps: &Captures<'_>| match &caps[2] {
144            "TRUE" => "[`true`]".to_string(),
145            "FALSE" => "[`false`]".to_string(),
146            "NULL" => "[`None`]".to_string(),
147            symbol_name => match &caps[1] {
148                // Opt-in only for the %SYMBOLS, @/# causes breakages
149                "%" => find_constant_or_variant_wrapper(symbol_name, env, in_type),
150                s => panic!("Unknown symbol prefix `{s}`"),
151            },
152        });
153        let out = gdk_gtk().replace_all(&out, |caps: &Captures<'_>| {
154            find_type(&caps[2], env).unwrap_or_else(|| format!("`{}`", &caps[2]))
155        });
156
157        out.to_string()
158    } else {
159        replace_c_types(input, env, in_type)
160    }
161}
162
163fn symbol() -> &'static Regex {
164    static REGEX: OnceLock<Regex> = OnceLock::new();
165    REGEX.get_or_init(|| Regex::new(r"([@#%])(\w+\b)([:.]+[\w-]+\b)?").unwrap())
166}
167
168fn gi_docgen_symbol() -> &'static Regex {
169    static REGEX: OnceLock<Regex> = OnceLock::new();
170    REGEX.get_or_init(|| Regex::new(r"([%])(\w+\b)([:.]+[\w-]+\b)?").unwrap())
171}
172
173fn function() -> &'static Regex {
174    static REGEX: OnceLock<Regex> = OnceLock::new();
175    REGEX.get_or_init(|| Regex::new(r"([@#%])?(\w+\b[:.]+)?(\b[a-z0-9_]+)\(\)").unwrap())
176}
177
178fn gdk_gtk() -> &'static Regex {
179    // **note**
180    // The optional . at the end is to make the regex more relaxed for some weird
181    // broken cases on gtk3's docs it doesn't hurt other docs so please don't drop
182    // it
183    static REGEX: OnceLock<Regex> = OnceLock::new();
184    REGEX.get_or_init(|| {
185        Regex::new(r"`([^\(:])?((G[dts]k|Pango|cairo_|graphene_|Adw|Hdy|GtkSource)\w+\b)(\.)?`")
186            .unwrap()
187    })
188}
189
190fn tags() -> &'static Regex {
191    static REGEX: OnceLock<Regex> = OnceLock::new();
192    REGEX.get_or_init(|| Regex::new(r"<[\w/-]+>").unwrap())
193}
194
195fn spaces() -> &'static Regex {
196    static REGEX: OnceLock<Regex> = OnceLock::new();
197    REGEX.get_or_init(|| Regex::new(r"[ ]{2,}").unwrap())
198}
199
200fn replace_c_types(
201    entry: &str,
202    env: &Env,
203    in_type: Option<(&TypeId, Option<LocationInObject>)>,
204) -> String {
205    let out = function().replace_all(entry, |caps: &Captures<'_>| {
206        let name = &caps[3];
207        find_method_or_function_by_ctype(None, name, env, in_type).unwrap_or_else(|| {
208            if !IGNORE_C_WARNING_FUNCS.contains(&name) {
209                info!("No function found for `{}()`", name);
210            }
211            format!("`{}{}()`", caps.get(2).map_or("", |m| m.as_str()), name)
212        })
213    });
214
215    let out = symbol().replace_all(&out, |caps: &Captures<'_>| match &caps[2] {
216        "TRUE" => "[`true`]".to_string(),
217        "FALSE" => "[`false`]".to_string(),
218        "NULL" => "[`None`]".to_string(),
219        symbol_name => match &caps[1] {
220            "%" => find_constant_or_variant_wrapper(symbol_name, env, in_type),
221            "#" => {
222                if let Some(member_path) = caps.get(3).map(|m| m.as_str()) {
223                    let method_name = member_path.trim_start_matches('.');
224                    find_member(symbol_name, method_name, env, in_type).unwrap_or_else(|| {
225                        info!("`#{}` not found as method", symbol_name);
226                        format!("`{symbol_name}{member_path}`")
227                    })
228                } else if let Some(type_) = find_type(symbol_name, env) {
229                    type_
230                } else if let Some(constant_or_variant) =
231                    find_constant_or_variant(symbol_name, env, in_type)
232                {
233                    warn!(
234                        "`{}` matches a constant/variant and should use `%` prefix instead of `#`",
235                        symbol_name
236                    );
237                    constant_or_variant
238                } else {
239                    info!("Type `#{}` not found", symbol_name);
240                    format!("`{symbol_name}`")
241                }
242            }
243            "@" => {
244                // XXX: Theoretically this code should check if the resulting
245                // symbol truly belongs to `in_type`!
246                if let Some(type_) = find_type(symbol_name, env) {
247                    warn!(
248                        "`{}` matches a type and should use `#` prefix instead of `%`",
249                        symbol_name
250                    );
251                    type_
252                } else if let Some(constant_or_variant) =
253                    find_constant_or_variant(symbol_name, env, in_type)
254                {
255                    constant_or_variant
256                } else if let Some(function) =
257                    find_method_or_function_by_ctype(None, symbol_name, env, in_type)
258                {
259                    function
260                } else {
261                    // `@` is often used to refer to fields and function parameters.
262                    format!("`{symbol_name}`")
263                }
264            }
265            s => panic!("Unknown symbol prefix `{s}`"),
266        },
267    });
268    let out = gdk_gtk().replace_all(&out, |caps: &Captures<'_>| {
269        find_type(&caps[2], env).unwrap_or_else(|| format!("`{}`", &caps[2]))
270    });
271    let out = tags().replace_all(&out, "`$0`");
272    spaces().replace_all(&out, " ").into_owned()
273}
274
275/// Wrapper around [`find_constant_or_variant`] that fallbacks to returning
276/// the `symbol_name`
277fn find_constant_or_variant_wrapper(
278    symbol_name: &str,
279    env: &Env,
280    in_type: Option<(&TypeId, Option<LocationInObject>)>,
281) -> String {
282    find_constant_or_variant(symbol_name, env, in_type).unwrap_or_else(|| {
283        info!("Constant or variant `%{}` not found", symbol_name);
284        format!("`{symbol_name}`")
285    })
286}
287
288fn find_member(
289    type_: &str,
290    method_name: &str,
291    env: &Env,
292    in_type: Option<(&TypeId, Option<LocationInObject>)>,
293) -> Option<String> {
294    let symbols = env.symbols.borrow();
295    let is_signal = method_name.starts_with("::");
296    let is_property = !is_signal && method_name.starts_with(':');
297    if !is_signal && !is_property {
298        find_method_or_function_by_ctype(Some(type_), method_name, env, in_type)
299    } else {
300        env.analysis
301            .objects
302            .values()
303            .find(|o| o.c_type == type_)
304            .map(|info| {
305                let sym = symbols.by_tid(info.type_id).unwrap(); // we are sure the object exists
306                let name = method_name.trim_start_matches(':');
307                if is_property {
308                    gen_property_doc_link(&sym.full_rust_name(), name)
309                } else {
310                    gen_signal_doc_link(&sym.full_rust_name(), name)
311                }
312            })
313    }
314}
315
316fn find_constant_or_variant(
317    symbol: &str,
318    env: &Env,
319    in_type: Option<(&TypeId, Option<LocationInObject>)>,
320) -> Option<String> {
321    if let Some((flag_info, member_info)) = env.analysis.flags.iter().find_map(|f| {
322        f.type_(&env.library)
323            .members
324            .iter()
325            .find(|m| m.c_identifier == symbol && !m.status.ignored())
326            .map(|m| (f, m))
327    }) {
328        Some(gen_member_doc_link(
329            flag_info.type_id,
330            &nameutil::bitfield_member_name(&member_info.name),
331            env,
332            in_type,
333        ))
334    } else if let Some((enum_info, member_info)) = env.analysis.enumerations.iter().find_map(|e| {
335        e.type_(&env.library)
336            .members
337            .iter()
338            .find(|m| m.c_identifier == symbol && !m.status.ignored())
339            .map(|m| (e, m))
340    }) {
341        Some(gen_member_doc_link(
342            enum_info.type_id,
343            &nameutil::enum_member_name(&member_info.name),
344            env,
345            in_type,
346        ))
347    } else if let Some(const_info) = env
348        .analysis
349        .constants
350        .iter()
351        .find(|c| c.glib_name == symbol)
352    {
353        Some(gen_const_doc_link(const_info))
354    } else {
355        None
356    }
357}
358
359// A list of types that are automatically ignored by the `find_type` function
360const IGNORED_C_TYPES: [&str; 6] = [
361    "gconstpointer",
362    "guint16",
363    "guint",
364    "gunicode",
365    "gchararray",
366    "GList",
367];
368/// either an object/interface, record, enum or a flag
369fn find_type(type_: &str, env: &Env) -> Option<String> {
370    if IGNORED_C_TYPES.contains(&type_) {
371        return None;
372    }
373
374    let type_id = if let Some(obj) = env.analysis.objects.values().find(|o| o.c_type == type_) {
375        Some(obj.type_id)
376    } else if let Some(record) = env
377        .analysis
378        .records
379        .values()
380        .find(|r| r.type_(&env.library).c_type == type_)
381    {
382        Some(record.type_id)
383    } else if let Some(enum_) = env
384        .analysis
385        .enumerations
386        .iter()
387        .find(|e| e.type_(&env.library).c_type == type_)
388    {
389        Some(enum_.type_id)
390    } else if let Some(flag) = env
391        .analysis
392        .flags
393        .iter()
394        .find(|f| f.type_(&env.library).c_type == type_)
395    {
396        Some(flag.type_id)
397    } else {
398        None
399    };
400
401    type_id.map(|ty| gen_symbol_doc_link(ty, env))
402}
403
404fn find_method_or_function_by_ctype(
405    c_type: Option<&str>,
406    name: &str,
407    env: &Env,
408    in_type: Option<(&TypeId, Option<LocationInObject>)>,
409) -> Option<String> {
410    find_method_or_function(
411        env,
412        in_type,
413        |f| f.glib_name == name,
414        |o| c_type.is_none_or(|t| o.c_type == t),
415        |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
416        |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
417        |r| c_type.is_none_or(|t| r.type_(&env.library).c_type == t),
418        c_type.is_some_and(|t| t.ends_with("Class")),
419        false,
420    )
421}
422
423/// Find a function in all the possible items, if not found return the original
424/// name surrounded with backticks. A function can either be a
425/// struct/interface/record method, a global function or maybe a virtual
426/// function
427///
428/// This function is generic so it can be de-duplicated between a
429/// - [`find_method_or_function_by_ctype()`] where the object/records are looked
430///   by their C name
431/// - [`gi_docgen::find_method_or_function_by_name()`] where the object/records
432///   are looked by their name
433pub(crate) fn find_method_or_function(
434    env: &Env,
435    in_type: Option<(&TypeId, Option<LocationInObject>)>,
436    search_fn: impl Fn(&crate::analysis::functions::Info) -> bool + Copy,
437    search_obj: impl Fn(&crate::analysis::object::Info) -> bool + Copy,
438    search_record: impl Fn(&crate::analysis::record::Info) -> bool + Copy,
439    search_enum: impl Fn(&crate::analysis::enums::Info) -> bool + Copy,
440    search_flag: impl Fn(&crate::analysis::flags::Info) -> bool + Copy,
441    is_class_method: bool,
442    is_virtual_method: bool,
443) -> Option<String> {
444    if is_virtual_method {
445        if let Some((obj_info, fn_info)) = env
446            .analysis
447            .find_object_by_virtual_method(env, search_obj, search_fn)
448        {
449            Some(gen_object_fn_doc_link(
450                obj_info,
451                fn_info,
452                env,
453                in_type,
454                &obj_info.name,
455            ))
456        } else {
457            None
458        }
459    } else if is_class_method {
460        if let Some((record_info, fn_info)) =
461            env.analysis
462                .find_record_by_function(env, search_record, search_fn)
463        {
464            let object = env.config.objects.get(&record_info.full_name);
465            let visible_parent = object
466                .and_then(|o| o.trait_name.clone())
467                .unwrap_or_else(|| format!("{}Ext", record_info.name));
468            let parent = format!("subclass::prelude::{}", visible_parent);
469            let is_self = in_type == Some((&record_info.type_id, None));
470            Some(fn_info.doc_link(Some(&parent), Some(&visible_parent), is_self))
471        } else {
472            None
473        }
474    // if we can find the function in an object
475    } else if let Some((obj_info, fn_info)) = env
476        .analysis
477        .find_object_by_function(env, search_obj, search_fn)
478    {
479        Some(gen_object_fn_doc_link(
480            obj_info,
481            fn_info,
482            env,
483            in_type,
484            &obj_info.name,
485        ))
486    // or in a record
487    } else if let Some((record_info, fn_info)) =
488        env.analysis
489            .find_record_by_function(env, search_record, search_fn)
490    {
491        Some(gen_type_fn_doc_link(
492            record_info.type_id,
493            fn_info,
494            env,
495            in_type,
496        ))
497    } else if let Some((enum_info, fn_info)) =
498        env.analysis
499            .find_enum_by_function(env, search_enum, search_fn)
500    {
501        Some(gen_type_fn_doc_link(
502            enum_info.type_id,
503            fn_info,
504            env,
505            in_type,
506        ))
507    } else if let Some((flag_info, fn_info)) =
508        env.analysis
509            .find_flag_by_function(env, search_flag, search_fn)
510    {
511        Some(gen_type_fn_doc_link(
512            flag_info.type_id,
513            fn_info,
514            env,
515            in_type,
516        ))
517    // or as a global function
518    } else if let Some(fn_info) = env.analysis.find_global_function(env, search_fn) {
519        Some(fn_info.doc_link(None, None, false))
520    } else {
521        None
522    }
523}
524
525pub(crate) fn gen_type_fn_doc_link(
526    type_id: TypeId,
527    fn_info: &Info,
528    env: &Env,
529    in_type: Option<(&TypeId, Option<LocationInObject>)>,
530) -> String {
531    let symbols = env.symbols.borrow();
532    let sym_name = symbols.by_tid(type_id).unwrap().full_rust_name();
533    let is_self = in_type == Some((&type_id, None));
534
535    fn_info.doc_link(Some(&sym_name), None, is_self)
536}
537
538pub(crate) fn gen_object_fn_doc_link(
539    obj_info: &crate::analysis::object::Info,
540    fn_info: &Info,
541    env: &Env,
542    in_type: Option<(&TypeId, Option<LocationInObject>)>,
543    visible_name: &str,
544) -> String {
545    let symbols = env.symbols.borrow();
546    let sym = symbols.by_tid(obj_info.type_id).unwrap();
547    let is_self = in_type == Some((&obj_info.type_id, Some(obj_info.function_location(fn_info))));
548
549    if fn_info.kind == FunctionKind::VirtualMethod || fn_info.kind == FunctionKind::ClassMethod {
550        let (type_name, visible_type_name) = obj_info.generate_doc_link_info(fn_info);
551        fn_info.doc_link(
552            Some(&sym.full_rust_name().replace(visible_name, &type_name)),
553            Some(&visible_type_name),
554            false,
555        )
556    } else if fn_info.kind == FunctionKind::Method {
557        let (type_name, visible_type_name) = obj_info.generate_doc_link_info(fn_info);
558
559        fn_info.doc_link(
560            Some(&sym.full_rust_name().replace(visible_name, &type_name)),
561            Some(&visible_type_name),
562            is_self,
563        )
564    } else {
565        fn_info.doc_link(Some(&sym.full_rust_name()), None, is_self)
566    }
567}
568
569// Helper function to generate a doc link for an enum member/bitfield variant
570pub(crate) fn gen_member_doc_link(
571    type_id: TypeId,
572    member_name: &str,
573    env: &Env,
574    in_type: Option<(&TypeId, Option<LocationInObject>)>,
575) -> String {
576    let symbols = env.symbols.borrow();
577    let sym = symbols.by_tid(type_id).unwrap().full_rust_name();
578    let is_self = in_type == Some((&type_id, None));
579
580    if is_self {
581        format!("[`{member_name}`][Self::{member_name}]")
582    } else {
583        format!("[`{sym}::{member_name}`][crate::{sym}::{member_name}]")
584    }
585}
586
587pub(crate) fn gen_const_doc_link(const_info: &crate::analysis::constants::Info) -> String {
588    // for whatever reason constants are not part of the symbols list
589    format!("[`{n}`][crate::{n}]", n = const_info.name)
590}
591
592pub(crate) fn gen_signal_doc_link(symbol: &str, signal: &str) -> String {
593    format!("[`{signal}`][struct@crate::{symbol}#{signal}]")
594}
595
596pub(crate) fn gen_property_doc_link(symbol: &str, property: &str) -> String {
597    format!("[`{property}`][struct@crate::{symbol}#{property}]")
598}
599
600pub(crate) fn gen_vfunc_doc_link(symbol: &str, vfunc: &str) -> String {
601    format!("`vfunc::{symbol}::{vfunc}`")
602}
603
604pub(crate) fn gen_callback_doc_link(callback: &str) -> String {
605    format!("`callback::{callback}")
606}
607
608pub(crate) fn gen_alias_doc_link(alias: &str) -> String {
609    format!("`alias::{alias}`")
610}
611
612pub(crate) fn gen_symbol_doc_link(type_id: TypeId, env: &Env) -> String {
613    let symbols = env.symbols.borrow();
614    let sym = symbols.by_tid(type_id).unwrap();
615    // Workaround the case of glib::Variant being a derive macro and a struct
616    if sym.name() == "Variant" && (sym.crate_name().is_none() || sym.crate_name() == Some("glib")) {
617        format!("[`{n}`][struct@crate::{n}]", n = sym.full_rust_name())
618    } else {
619        format!("[`{n}`][crate::{n}]", n = sym.full_rust_name())
620    }
621}