wowlab_tidy/languages/toml/
mod.rs1mod 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 && let Some(RuleFix::Toml(fix)) = rule.fix {
60 analysis.fixes.extend(
61 violations
62 .iter()
63 .filter_map(|violation| fix(&ctx, violation))
64 .map(|fix| (file.rel.to_owned(), fix)),
65 );
66 }
67
68 analysis.violations.extend(violations);
69 }
70
71 analysis
72}
73
74fn workspace_manifest(ctx: &TomlCtx<'_>) -> Option<crate::languages::workspace::WorkspaceManifest> {
75 if ctx.file.path.file_name()?.to_str()? != "Cargo.toml" || !ctx.parse.errors.is_empty() {
76 return None;
77 }
78
79 let document = toml::from_str::<toml::Table>(ctx.file.contents).ok()?;
80
81 if !document.contains_key("package") {
82 return None;
83 }
84
85 let mut dependencies = Vec::new();
86
87 extract_dependency_sections(ctx, &document, &mut dependencies);
88
89 if let Some(targets) = document.get("target").and_then(toml::Value::as_table) {
90 for target in targets.values().filter_map(toml::Value::as_table) {
91 extract_dependency_sections(ctx, target, &mut dependencies);
92 }
93 }
94
95 Some(crate::languages::workspace::WorkspaceManifest {
96 rel: ctx.file.rel.to_owned(),
97 dependencies,
98 })
99}
100
101fn extract_dependency_sections(
102 ctx: &TomlCtx<'_>,
103 document: &toml::Table,
104 out: &mut Vec<crate::languages::workspace::DependencyRecord>,
105) {
106 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
107 if let Some(table) = document.get(section).and_then(toml::Value::as_table) {
108 extract_dependencies(ctx, section, table, out);
109 }
110 }
111}
112
113fn extract_dependencies(
114 ctx: &TomlCtx<'_>,
115 section: &str,
116 table: &toml::Table,
117 out: &mut Vec<crate::languages::workspace::DependencyRecord>,
118) {
119 for name in table.keys() {
121 out.push(crate::languages::workspace::DependencyRecord {
122 name: name.clone(),
123 root: name.replace('-', "_"),
124 line: dependency_line(ctx.file.lines, section, name),
125 });
126 }
127}
128
129fn dependency_line(lines: &[&str], section: &str, name: &str) -> usize {
130 let mut in_section = false;
131
132 for (index, line) in lines.iter().enumerate() {
133 let trimmed = line.trim();
134
135 if let Some(header) = trimmed
136 .strip_prefix('[')
137 .and_then(|header| header.strip_suffix(']'))
138 {
139 in_section = header == section || header.ends_with(&format!(".{section}"));
141 continue;
142 }
143
144 if in_section
145 && trimmed
146 .split_once('=')
147 .is_some_and(|(key, _)| key.trim().trim_matches('"') == name)
148 {
149 return index + 1;
150 }
151 }
152
153 1
154}