miniextendr_lint/
crate_root.rs1use std::fmt;
17use std::fs;
18use std::path::{Path, PathBuf};
19
20const LIB_PATH_KEY: &str = "lib.path";
22
23#[derive(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for RootOrigin { }
#[automatically_derived]
impl ::core::clone::Clone for RootOrigin {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for RootOrigin { }Copy, #[automatically_derived]
impl ::core::fmt::Debug for RootOrigin {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
RootOrigin::Manifest => "Manifest",
RootOrigin::Default => "Default",
})
}
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for RootOrigin { }
#[automatically_derived]
impl ::core::cmp::PartialEq for RootOrigin {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for RootOrigin { }Eq)]
25pub enum RootOrigin {
26 Manifest,
28 Default,
30}
31
32#[derive(#[automatically_derived]
impl ::core::clone::Clone for CrateRoot {
#[inline]
fn clone(&self) -> Self {
CrateRoot {
file: ::core::clone::Clone::clone(&self.file),
origin: ::core::clone::Clone::clone(&self.origin),
}
}
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for CrateRoot {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "CrateRoot",
"file", &self.file, "origin", &&self.origin)
}
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for CrateRoot { }
#[automatically_derived]
impl ::core::cmp::PartialEq for CrateRoot {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.file == other.file && self.origin == other.origin
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CrateRoot {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<PathBuf>;
let _: ::core::cmp::AssertParamIsEq<RootOrigin>;
}
}Eq)]
34pub struct CrateRoot {
35 pub file: PathBuf,
37 pub origin: RootOrigin,
39}
40
41impl CrateRoot {
42 pub fn resolve(manifest_dir: &Path) -> Result<Self, String> {
48 let manifest = manifest_dir.join("Cargo.toml");
49 let lib_path = match fs::read_to_string(&manifest) {
50 Ok(text) => lib_path_from_manifest(&text).map_err(|message| {
51 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("miniextendr-lint: {0}: {1}",
manifest.display(), message))
})format!("miniextendr-lint: {}: {message}", manifest.display())
52 })?,
53 Err(_) => None,
54 };
55
56 Ok(match lib_path {
57 Some(path) => Self {
58 file: normalize(&manifest_dir.join(path)),
59 origin: RootOrigin::Manifest,
60 },
61 None => {
62 let src_dir = manifest_dir.join("src");
63 let dir = if src_dir.is_dir() {
64 src_dir
65 } else {
66 manifest_dir.to_path_buf()
67 };
68 Self {
69 file: dir.join("lib.rs"),
70 origin: RootOrigin::Default,
71 }
72 }
73 })
74 }
75}
76
77impl fmt::Display for RootOrigin {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.write_str(match self {
80 Self::Manifest => "[lib] path in Cargo.toml",
81 Self::Default => "no [lib] path in Cargo.toml",
82 })
83 }
84}
85
86pub(crate) fn prose_links_keep(manifest_dir: &Path) -> bool {
91 const KEY: &str = "package.metadata.miniextendr.roxygen_prose_links";
92 let Ok(text) = fs::read_to_string(manifest_dir.join("Cargo.toml")) else {
93 return false;
94 };
95 let mut table = String::new();
96 for raw_line in text.lines() {
97 let line = raw_line.trim();
98 if line.is_empty() || line.starts_with('#') {
99 continue;
100 }
101 if let Some(header) = line.strip_prefix('[') {
102 let header = header.strip_prefix('[').unwrap_or(header);
103 if let Some(end) = header.find(']') {
104 table = normalize_key(&header[..end]);
105 }
106 continue;
107 }
108 let Some((key, value)) = line.split_once('=') else {
109 continue;
110 };
111 let key = normalize_key(key);
112 let full = if table.is_empty() {
113 key
114 } else {
115 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.{1}", table, key))
})format!("{table}.{key}")
116 };
117 if full == KEY {
118 return parse_string_value(value.trim()).is_some_and(|v| v == "keep");
119 }
120 }
121 false
122}
123
124fn normalize(path: &Path) -> PathBuf {
127 path.components().collect()
128}
129
130fn lib_path_from_manifest(text: &str) -> Result<Option<String>, String> {
135 let mut table = String::new();
136
137 for raw_line in text.lines() {
138 let line = raw_line.trim();
139 if line.is_empty() || line.starts_with('#') {
140 continue;
141 }
142 if let Some(header) = line.strip_prefix('[') {
143 let header = header.strip_prefix('[').unwrap_or(header);
148 if let Some(end) = header.find(']') {
149 table = normalize_key(&header[..end]);
150 }
151 continue;
152 }
153 let Some((key, value)) = line.split_once('=') else {
154 continue;
155 };
156 let key = normalize_key(key);
157 let full = if table.is_empty() {
158 key
159 } else {
160 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.{1}", table, key))
})format!("{table}.{key}")
161 };
162 let value = value.trim();
163 if full == "lib" && value.starts_with('{') && inline_table_has_path(value) {
164 return Err(
165 "an inline `lib = { path = ... }` table is not read; write a `[lib]` table with \
166 `path = \"...\"` on its own line"
167 .to_string(),
168 );
169 }
170 if full != LIB_PATH_KEY {
171 continue;
172 }
173 return parse_string_value(value).map(Some).ok_or_else(|| {
174 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("cannot read [lib] path `{0}`; write it as a one-line string without escapes",
value))
})format!(
175 "cannot read [lib] path `{value}`; write it as a one-line string without escapes"
176 )
177 });
178 }
179
180 Ok(None)
181}
182
183fn inline_table_has_path(value: &str) -> bool {
186 value
187 .trim_start_matches('{')
188 .split(',')
189 .filter_map(|piece| piece.split_once('='))
190 .any(|(key, _)| normalize_key(key) == "path")
191}
192
193fn normalize_key(key: &str) -> String {
196 key.split('.')
197 .map(|segment| {
198 let segment = segment.trim();
199 segment
200 .strip_prefix('"')
201 .and_then(|s| s.strip_suffix('"'))
202 .or_else(|| {
203 segment
204 .strip_prefix('\'')
205 .and_then(|s| s.strip_suffix('\''))
206 })
207 .unwrap_or(segment)
208 })
209 .collect::<Vec<_>>()
210 .join(".")
211}
212
213fn parse_string_value(value: &str) -> Option<String> {
219 let (quote, rest) = match value.chars().next()? {
220 c @ ('"' | '\'') => (c, &value[1..]),
221 _ => return None,
222 };
223 if rest.starts_with(quote) && rest[1..].starts_with(quote) {
224 return None;
226 }
227 let end = rest.find(quote)?;
228 let body = &rest[..end];
229 if body.contains('\\') && quote == '"' {
230 return None;
231 }
232 let trailing = rest[end + 1..].trim_start();
233 if !(trailing.is_empty() || trailing.starts_with('#')) {
234 return None;
235 }
236 Some(body.to_string())
237}