use line_index::LineIndex;
use ra_ap_syntax::{Edition, SourceFile};
use crate::{AstCtx, Config, FileCtx, Fix, Path, TomlCtx, Violation};
#[must_use]
pub fn check_workspace_sources(
sources: &[(&str, &str)],
check: fn(&crate::WorkspaceCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
check_workspace_sources_with_config(sources, &Config::generate_default(&[]), check)
}
#[must_use]
pub fn check_workspace_sources_with_config(
sources: &[(&str, &str)],
config: &Config,
check: fn(&crate::WorkspaceCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
let mut files = Vec::new();
let mut manifests = Vec::new();
let mut paths = sources
.iter()
.map(|(rel, _)| (*rel).to_owned())
.collect::<Vec<_>>();
paths.sort();
paths.dedup();
for &(rel, contents) in sources {
let lines = contents.lines().collect::<Vec<_>>();
let file = FileCtx {
test_only: false,
rel,
path: Path::new(rel),
package_name: None,
package_publishable: None,
lines: &lines,
contents,
config,
};
if Path::new(rel)
.extension()
.is_some_and(|extension| extension == "rs")
{
let parse = SourceFile::parse(contents, Edition::Edition2024);
let mut errors = Vec::new();
let suppressions =
crate::infra::ignore::suppressed_lines(rel, &lines, &mut errors, None);
files.push(crate::languages::workspace::extract(
&file,
&parse.tree(),
suppressions,
false,
));
} else if rel.ends_with("Cargo.toml") {
let parse = taplo::parser::parse(contents);
let dom = parse.clone().into_dom();
if let Some(manifest) = crate::languages::toml::workspace_manifest(&TomlCtx {
file: &file,
parse: &parse,
dom: &dom,
}) {
manifests.push(manifest);
}
}
}
files.sort_by(|a, b| a.rel.cmp(&b.rel));
manifests.sort_by(|a, b| a.rel.cmp(&b.rel));
check(&crate::WorkspaceCtx {
paths: &paths,
files: &files,
manifests: &manifests,
config,
})
}
fn package_name_from_path(rel: &str) -> Option<&str> {
let components: Vec<&str> = rel.split('/').collect();
let source = components
.iter()
.position(|component| *component == "src")?;
source
.checked_sub(1)
.and_then(|index| components.get(index).copied())
.or(Some("fixture"))
}
#[must_use]
pub fn check_source(source: &str, check: fn(&FileCtx<'_>) -> Vec<Violation>) -> Vec<Violation> {
check_source_at("test.rs", source, check)
}
#[must_use]
pub fn check_source_at(
rel: &str,
source: &str,
check: fn(&FileCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let context = FileCtx {
test_only: false,
rel,
path: Path::new(rel),
package_name: package_name_from_path(rel),
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
check(&context)
}
#[must_use]
pub fn check_source_ast(source: &str, check: fn(&AstCtx<'_>) -> Vec<Violation>) -> Vec<Violation> {
check_source_ast_at("test.rs", source, check)
}
#[must_use]
pub fn check_source_ast_at(
rel: &str,
source: &str,
check: fn(&AstCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
check_source_ast_at_with_publishability(rel, source, None, check)
}
pub(crate) fn check_source_ast_at_with_publishability(
rel: &str,
source: &str,
package_publishable: Option<bool>,
check: fn(&AstCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let file = FileCtx {
test_only: false,
rel,
path: Path::new(rel),
package_name: package_name_from_path(rel),
package_publishable,
lines: &lines,
contents: source,
config: &config,
};
let parse = SourceFile::parse(source, Edition::Edition2024);
assert!(parse.errors().is_empty(), "test source must parse");
let root = parse.tree();
let line_index = LineIndex::new(source);
let context = AstCtx::new(&file, &root, &line_index, false);
check(&context)
}
#[must_use]
pub fn check_source_toml(
source: &str,
check: fn(&TomlCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
check_source_toml_at("config/policy.toml", source, check)
}
#[must_use]
pub fn check_source_toml_at(
rel: &str,
source: &str,
check: fn(&TomlCtx<'_>) -> Vec<Violation>,
) -> Vec<Violation> {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let file = FileCtx {
test_only: false,
rel,
path: Path::new(rel),
package_name: None,
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
let parse = taplo::parser::parse(source);
let dom = parse.clone().into_dom();
check(&TomlCtx {
file: &file,
parse: &parse,
dom: &dom,
})
}
fn apply_fixes_to_source(source: &str, mut fixes: Vec<Fix>) -> String {
if let [replacement] = fixes.as_slice()
&& replacement.start_line == 1
&& replacement.end_line >= source.lines().count().max(1)
{
return replacement.replacement.clone();
}
fixes.sort_by_key(|fix| std::cmp::Reverse(fix.start_line));
let mut lines: Vec<String> = source.lines().map(String::from).collect();
for fix in fixes {
let start = fix.start_line.saturating_sub(1);
let end = fix.end_line.min(lines.len());
if start >= lines.len() || start >= end {
continue;
}
if fix.replacement.is_empty() {
lines.drain(start..end);
} else {
lines.splice(start..end, fix.replacement.lines().map(String::from));
}
}
lines.join("\n")
}
#[must_use]
pub fn apply_line_fixes(
source: &str,
check: fn(&FileCtx<'_>) -> Vec<Violation>,
fix: fn(&FileCtx<'_>, &Violation) -> Option<Fix>,
) -> String {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let context = FileCtx {
test_only: false,
rel: "test.rs",
path: Path::new("test.rs"),
package_name: None,
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
let fixes = check(&context)
.iter()
.filter_map(|violation| fix(&context, violation))
.collect();
apply_fixes_to_source(source, fixes)
}
#[must_use]
pub fn apply_ast_fixes(
source: &str,
check: fn(&AstCtx<'_>) -> Vec<Violation>,
fix: fn(&AstCtx<'_>, &Violation) -> Option<Fix>,
) -> String {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let file = FileCtx {
test_only: false,
rel: "test.rs",
path: Path::new("test.rs"),
package_name: None,
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
let parse = SourceFile::parse(source, Edition::Edition2024);
assert!(parse.errors().is_empty(), "test source must parse");
let root = parse.tree();
let line_index = LineIndex::new(source);
let context = AstCtx::new(&file, &root, &line_index, false);
let fixes = check(&context)
.iter()
.filter_map(|violation| fix(&context, violation))
.collect();
apply_fixes_to_source(source, fixes)
}
#[must_use]
pub fn apply_ast_tree_fix(
source: &str,
check: fn(&AstCtx<'_>) -> Vec<Violation>,
fix: fn(&AstCtx<'_>, &[Violation]) -> Option<String>,
) -> String {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let file = FileCtx {
test_only: false,
rel: "test.rs",
path: Path::new("test.rs"),
package_name: None,
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
let parse = SourceFile::parse(source, Edition::Edition2024);
assert!(parse.errors().is_empty(), "test source must parse");
let root = parse.tree();
let line_index = LineIndex::new(source);
let context = AstCtx::new(&file, &root, &line_index, false);
let violations = check(&context);
fix(&context, &violations).unwrap_or_else(|| source.to_owned())
}
#[must_use]
pub fn apply_toml_fixes(
source: &str,
check: fn(&TomlCtx<'_>) -> Vec<Violation>,
fix: fn(&TomlCtx<'_>, &Violation) -> Option<Fix>,
) -> String {
apply_toml_fixes_at("config/policy.toml", source, check, fix)
}
#[must_use]
pub fn apply_toml_fixes_at(
rel: &str,
source: &str,
check: fn(&TomlCtx<'_>) -> Vec<Violation>,
fix: fn(&TomlCtx<'_>, &Violation) -> Option<Fix>,
) -> String {
let config = Config::generate_default(&[]);
let lines: Vec<&str> = source.lines().collect();
let file = FileCtx {
test_only: false,
rel,
path: Path::new(rel),
package_name: None,
package_publishable: None,
lines: &lines,
contents: source,
config: &config,
};
let parse = taplo::parser::parse(source);
let dom = parse.clone().into_dom();
let context = TomlCtx {
file: &file,
parse: &parse,
dom: &dom,
};
let fixes = check(&context)
.iter()
.filter_map(|violation| fix(&context, violation))
.collect();
apply_fixes_to_source(source, fixes)
}