Skip to main content

wowlab_tidy/languages/toml/
mod.rs

1//! Taplo-backed TOML parsing and rule execution.
2
3mod rules;
4
5use crate::{FileCtx, Rule, RuleCheck, RuleFix, Violation, languages::Analysis, matches_ignore};
6
7#[derive(Debug)]
8pub(crate) struct TomlCtx<'a> {
9    pub(crate) file: &'a FileCtx<'a>,
10    pub(crate) parse: &'a taplo::parser::Parse,
11    pub(crate) dom: &'a taplo::dom::Node,
12}
13
14impl TomlCtx<'_> {
15    pub(crate) fn line_of_offset(&self, offset: usize) -> usize {
16        self.file
17            .contents
18            .get(..offset.min(self.file.contents.len()))
19            .unwrap_or(self.file.contents)
20            .bytes()
21            .filter(|byte| *byte == b'\n')
22            .count()
23            + 1
24    }
25}
26
27pub(crate) fn analyze(file: &FileCtx<'_>, rules: &[&Rule], fix_mode: bool) -> Analysis {
28    let parse = taplo::parser::parse(file.contents);
29    let dom = parse.clone().into_dom();
30    let ctx = TomlCtx {
31        file,
32        parse: &parse,
33        dom: &dom,
34    };
35    let mut analysis = Analysis::default();
36
37    if rules
38        .iter()
39        .any(|rule| matches!(rule.check, RuleCheck::Workspace(_)))
40        && let Some(manifest) = workspace_manifest(&ctx)
41    {
42        analysis.workspace_manifests.push(manifest);
43    }
44
45    for rule in rules {
46        let RuleCheck::Toml(check) = rule.check else {
47            continue;
48        };
49
50        if matches_ignore(file.rel, file.config.ignore_patterns(rule.info.name)) {
51            continue;
52        }
53
54        let violations: Vec<Violation> = check(&ctx)
55            .into_iter()
56            .map(|violation| violation.with_rule(rule.info.name))
57            .collect();
58
59        if fix_mode {
60            if let Some(RuleFix::Toml(fix)) = rule.fix {
61                analysis.fixes.extend(
62                    violations
63                        .iter()
64                        .filter_map(|violation| fix(&ctx, violation))
65                        .map(|fix| (file.rel.to_owned(), fix)),
66                );
67            }
68        }
69
70        analysis.violations.extend(violations);
71    }
72
73    analysis
74}
75
76fn workspace_manifest(ctx: &TomlCtx<'_>) -> Option<crate::languages::workspace::WorkspaceManifest> {
77    if ctx.file.path.file_name()?.to_str()? != "Cargo.toml" || !ctx.parse.errors.is_empty() {
78        return None;
79    }
80
81    let document = toml::from_str::<toml::Table>(ctx.file.contents).ok()?;
82
83    if !document.contains_key("package") {
84        return None;
85    }
86
87    let mut dependencies = Vec::new();
88
89    extract_dependency_sections(ctx, &document, &mut dependencies);
90
91    if let Some(targets) = document.get("target").and_then(toml::Value::as_table) {
92        for target in targets.values().filter_map(toml::Value::as_table) {
93            extract_dependency_sections(ctx, target, &mut dependencies);
94        }
95    }
96
97    Some(crate::languages::workspace::WorkspaceManifest {
98        rel: ctx.file.rel.to_owned(),
99        dependencies,
100    })
101}
102
103fn extract_dependency_sections(
104    ctx: &TomlCtx<'_>,
105    document: &toml::Table,
106    out: &mut Vec<crate::languages::workspace::DependencyRecord>,
107) {
108    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
109        if let Some(table) = document.get(section).and_then(toml::Value::as_table) {
110            extract_dependencies(ctx, section, table, out);
111        }
112    }
113}
114
115fn extract_dependencies(
116    ctx: &TomlCtx<'_>,
117    section: &str,
118    table: &toml::Table,
119    out: &mut Vec<crate::languages::workspace::DependencyRecord>,
120) {
121    // #t(block: rust_clone_in_loop) dependency records own manifest keys beyond the parsed table
122    for name in table.keys() {
123        out.push(crate::languages::workspace::DependencyRecord {
124            name: name.clone(),
125            root: name.replace('-', "_"),
126            line: dependency_line(ctx.file.lines, section, name),
127        });
128    }
129}
130
131fn dependency_line(lines: &[&str], section: &str, name: &str) -> usize {
132    let mut in_section = false;
133
134    for (index, line) in lines.iter().enumerate() {
135        let trimmed = line.trim();
136
137        if let Some(header) = trimmed
138            .strip_prefix('[')
139            .and_then(|header| header.strip_suffix(']'))
140        {
141            // #t(rust_alloc_in_loop) target-section suffix is rendered only for manifest headers
142            in_section = header == section || header.ends_with(&format!(".{section}"));
143            continue;
144        }
145
146        if in_section
147            && trimmed
148                .split_once('=')
149                .is_some_and(|(key, _)| key.trim().trim_matches('"') == name)
150        {
151            return index + 1;
152        }
153    }
154
155    1
156}