extern crate rustc_ast;
extern crate rustc_lint;
extern crate rustc_span;
use rustc_lint::LintContext;
use rustc_ast::{UseTree, UseTreeKind};
use rustc_span::source_map::SourceMap;
use rustc_span::{FileName, RemapPathScopeComponents, Span};
use std::collections::HashSet;
const ALLOWED_FLAGS: &[&str] = &["request", "response"];
pub fn is_in_domain_path(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/domain/")
}
pub fn is_in_infra_path(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/infra/")
}
pub fn is_in_contract_path(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/contract/")
}
pub fn is_in_contract_module_ast(
cx: &rustc_lint::EarlyContext<'_>,
item: &rustc_ast::Item,
) -> bool {
is_in_contract_path(cx.sess().source_map(), item.span)
}
pub fn is_in_domain_module_ast(cx: &rustc_lint::EarlyContext<'_>, item: &rustc_ast::Item) -> bool {
is_in_domain_path(cx.sess().source_map(), item.span)
}
pub fn is_in_api_rest_folder(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/api/rest/")
}
pub fn is_in_module_folder(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/modules/")
}
pub fn filename_str(source_map: &SourceMap, span: Span) -> Option<String> {
let file_name = source_map.span_to_filename(span);
match &file_name {
FileName::Real(real) => {
if let Some(local) = real.local_path() {
Some(local.to_string_lossy().to_string())
} else {
Some(
real.path(RemapPathScopeComponents::DIAGNOSTICS)
.to_string_lossy()
.to_string(),
)
}
}
_ => None,
}
}
pub fn is_temp_path(path: &str) -> bool {
let temp_dir = std::env::temp_dir();
if let Some(temp_str) = temp_dir.to_str()
&& path.starts_with(temp_str)
{
return true;
}
path.contains("/tmp/") || path.contains("/var/folders/") || path.contains("\\Temp\\")
}
pub struct VersionParts<'a> {
pub base: &'a str,
pub version_suffix: &'a str,
pub malformed_digits: &'a str,
}
impl VersionParts<'_> {
pub fn has_valid_version(&self) -> bool {
!self.version_suffix.is_empty()
}
pub fn has_malformed_version(&self) -> bool {
!self.malformed_digits.is_empty() && self.version_suffix.is_empty()
}
}
pub fn parse_version_suffix(name: &str) -> VersionParts<'_> {
if name.is_empty() {
return VersionParts {
base: name,
version_suffix: "",
malformed_digits: "",
};
}
let bytes = name.as_bytes();
let len = bytes.len();
let mut digit_count = 0;
for &b in bytes.iter().rev() {
if b.is_ascii_digit() {
digit_count += 1;
} else {
break;
}
}
if digit_count == 0 {
if len > 1 && bytes[len - 1] == b'V' {
return VersionParts {
base: &name[..len - 1],
version_suffix: "",
malformed_digits: "",
};
}
return VersionParts {
base: name,
version_suffix: "",
malformed_digits: "",
};
}
let digits_start = len - digit_count;
if digits_start > 0 && bytes[digits_start - 1] == b'V' {
let v_pos = digits_start - 1;
let digit_str = &name[digits_start..];
if !digit_str.starts_with('0') {
VersionParts {
base: &name[..v_pos],
version_suffix: &name[v_pos..],
malformed_digits: "",
}
} else {
VersionParts {
base: &name[..v_pos],
version_suffix: "",
malformed_digits: "",
}
}
} else {
VersionParts {
base: &name[..digits_start],
version_suffix: "",
malformed_digits: &name[digits_start..],
}
}
}
pub fn is_in_sdk_crate(cx: &rustc_lint::EarlyContext<'_>, span: Span) -> bool {
if let Some(crate_name) = cx.sess().opts.crate_name.as_deref()
&& (crate_name.ends_with("-sdk") || crate_name.ends_with("_sdk"))
{
return true;
}
let Some(file_path) = filename_str(cx.sess().source_map(), span) else {
return false;
};
file_path.contains("-sdk/") || file_path.contains("-sdk\\") || is_temp_path(&file_path)
}
pub fn is_in_modkit_db_path(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/libs/modkit-db/")
|| check_span_path(source_map, span, "libs/modkit-db/")
|| check_span_path(source_map, span, "modkit-db/src/")
}
pub fn is_in_cyberware_server_path(source_map: &SourceMap, span: Span) -> bool {
check_span_path(source_map, span, "/apps/cyberware-example-server/")
|| check_span_path(source_map, span, "apps/cyberware-example-server/")
|| check_span_path(source_map, span, "cyberware-example-server/src/")
}
pub fn check_derive_attrs<F>(item: &rustc_ast::Item, mut f: F)
where
F: FnMut(&rustc_ast::MetaItem, &rustc_ast::Attribute),
{
for attr in &item.attrs {
if !attr.has_name(rustc_span::symbol::sym::derive) {
continue;
}
if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind
&& let Some(meta_items) = attr_item.item.meta_item_list()
{
for nested_meta in meta_items {
if let Some(meta_item) = nested_meta.meta_item() {
f(meta_item, attr)
}
}
}
}
}
pub fn get_derive_path_segments(meta_item: &rustc_ast::MetaItem) -> Vec<&str> {
let path = &meta_item.path;
path.segments
.iter()
.map(|s| s.ident.name.as_str())
.collect()
}
pub fn is_serde_trait(segments: &[&str], trait_name: &str) -> bool {
if segments.is_empty() {
return false;
}
if segments.last() != Some(&trait_name) {
return false;
}
if segments.len() >= 2 {
segments.contains(&"serde")
} else {
true
}
}
pub fn has_api_dto_attribute(item: &rustc_ast::Item) -> bool {
for attr in &item.attrs {
if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind {
let path = &attr_item.item.path;
let segments: Vec<&str> = path
.segments
.iter()
.map(|s| s.ident.name.as_str())
.collect();
if segments.last() == Some(&"api_dto") {
return true;
}
}
}
false
}
pub fn get_api_dto_args(item: &rustc_ast::Item) -> Option<ApiDtoArgs> {
for attr in &item.attrs {
if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind {
let path = &attr_item.item.path;
let segments: Vec<&str> = path
.segments
.iter()
.map(|s| s.ident.name.as_str())
.collect();
if segments.last() != Some(&"api_dto") {
continue;
}
let mut has_request = false;
let mut has_response = false;
let mut seen_flags = HashSet::new();
let mut has_invalid = false;
if let Some(args) = attr_item.item.meta_item_list() {
for arg in args {
if let Some(ident) = arg.ident() {
let flag_str = ident.name.as_str();
if !ALLOWED_FLAGS.contains(&flag_str) {
has_invalid = true;
break;
}
if !seen_flags.insert(flag_str.to_string()) {
has_invalid = true;
break;
}
match flag_str {
"request" => has_request = true,
"response" => has_response = true,
_ => unreachable!(),
}
}
}
}
if has_invalid {
return None;
}
if !has_request && !has_response {
return None;
}
return Some(ApiDtoArgs {
has_request,
has_response,
});
}
}
None
}
#[derive(Debug, Clone, Copy)]
pub struct ApiDtoArgs {
pub has_request: bool,
pub has_response: bool,
}
impl ApiDtoArgs {
pub fn adds_serialize(&self) -> bool {
self.has_response
}
pub fn adds_deserialize(&self) -> bool {
self.has_request
}
pub fn adds_toschema(&self) -> bool {
true
}
pub fn adds_snake_case_rename(&self) -> bool {
self.has_request || self.has_response
}
}
pub fn is_utoipa_trait(segments: &[&str], trait_name: &str) -> bool {
if segments.is_empty() {
return false;
}
if segments.last() != Some(&trait_name) {
return false;
}
if segments.len() >= 2 {
segments.contains(&"utoipa")
} else {
true
}
}
pub fn use_tree_to_strings(tree: &UseTree) -> Vec<String> {
match &tree.kind {
UseTreeKind::Simple(..) | UseTreeKind::Glob => {
vec![
tree.prefix
.segments
.iter()
.map(|seg| seg.ident.name.as_str())
.collect::<Vec<_>>()
.join("::"),
]
}
UseTreeKind::Nested { items, .. } => {
let prefix = tree
.prefix
.segments
.iter()
.map(|seg| seg.ident.name.as_str())
.collect::<Vec<_>>()
.join("::");
let mut paths = Vec::new();
for (nested_tree, _) in items {
for nested_str in use_tree_to_strings(nested_tree) {
if nested_str.is_empty() {
paths.push(prefix.clone());
} else if prefix.is_empty() {
paths.push(nested_str);
} else {
paths.push(format!("{}::{}", prefix, nested_str));
}
}
}
if paths.is_empty() {
vec![prefix]
} else {
paths
}
}
}
}
fn check_span_path(source_map: &SourceMap, span: Span, pattern: &str) -> bool {
let pattern_windows = pattern.replace('/', "\\");
let Some(path_str) = get_path_str_from_session(source_map, span) else {
return false;
};
if let Some(simulated) = extract_simulated_dir(&path_str) {
return simulated.contains(pattern) || simulated.contains(&pattern_windows);
}
path_str.contains(pattern) || path_str.contains(&pattern_windows)
}
fn get_path_str_from_session(source_map: &SourceMap, span: Span) -> Option<String> {
let file_name = source_map.span_to_filename(span);
match file_name {
FileName::Real(ref real_name) => real_name
.local_path()
.map(|local| local.to_string_lossy().to_string()),
_ => None,
}
}
fn extract_simulated_dir(path_str: &str) -> Option<String> {
let is_temp = path_str.contains("/tmp/")
|| path_str.contains("/var/folders/") || path_str.contains("\\Temp\\") || path_str.contains(".tmp");
if !is_temp {
return None;
}
let contents = std::fs::read_to_string(std::path::PathBuf::from(path_str)).ok()?;
for line in contents.lines().take(1) {
let trimmed = line.trim();
if trimmed.starts_with("// simulated_dir=") {
return Some(trimmed.trim_start_matches("// simulated_dir=").to_string());
}
if !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#!") {
break;
}
}
None
}
pub fn test_comment_annotations_match_stderr(
ui_dir: &std::path::Path,
lint_code: &str,
comment_pattern: &str,
) {
use std::collections::{HashMap, HashSet};
use std::fs;
let trigger_comment = format!("// Should trigger {} - {}", lint_code, comment_pattern);
let not_trigger_comment = format!("// Should not trigger {} - {}", lint_code, comment_pattern);
let rs_files: Vec<_> = fs::read_dir(ui_dir)
.expect("Failed to read ui directory")
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension()? == "rs" {
Some(path)
} else {
None
}
})
.collect();
assert!(
!rs_files.is_empty(),
"No .rs test files found in ui directory"
);
for rs_file in rs_files {
let stderr_file = rs_file.with_extension("stderr");
let rs_content =
fs::read_to_string(&rs_file).unwrap_or_else(|_| panic!("Failed to read {:?}", rs_file));
let stderr_content = fs::read_to_string(&stderr_file).unwrap_or_default();
let rs_lines: Vec<&str> = rs_content.lines().collect();
let mut should_trigger_lines = HashMap::new();
let mut should_not_trigger_lines = HashMap::new();
for (idx, line) in rs_lines.iter().enumerate() {
let comment_line_num = idx + 1;
let expected_error_line_num = idx + 2;
if line.contains(&trigger_comment) {
should_trigger_lines.insert(expected_error_line_num, comment_line_num);
} else if line.contains(¬_trigger_comment) {
should_not_trigger_lines.insert(expected_error_line_num, comment_line_num);
}
}
let mut error_lines = HashSet::new();
for line in stderr_content.lines() {
if line.contains("-->")
&& line.contains(".rs:")
&& let Some(pos) = line.rfind(".rs:")
{
let rest = &line[pos + 4..];
if let Some(colon_pos) = rest.find(':')
&& let Ok(line_num) = rest[..colon_pos].parse::<usize>()
{
error_lines.insert(line_num);
}
}
}
for (line_num, comment_line_num) in &should_trigger_lines {
assert!(
error_lines.contains(line_num),
"In {:?}: Line {} has '{}' comment but no corresponding error in .stderr file",
rs_file.file_name().unwrap(),
comment_line_num,
trigger_comment
);
}
for (line_num, comment_line_num) in &should_not_trigger_lines {
assert!(
!error_lines.contains(line_num),
"In {:?}: Line {} has '{}' comment but has an error in .stderr file",
rs_file.file_name().unwrap(),
comment_line_num,
not_trigger_comment
);
}
for line_num in &error_lines {
assert!(
should_trigger_lines.contains_key(line_num),
"In {:?}: Line {} has an error in .stderr file but no '{}' comment",
rs_file.file_name().unwrap(),
line_num,
trigger_comment
);
}
}
}