use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProjectType {
Rust,
Node,
Python,
Go,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Strictness {
Strict,
Standard,
Relaxed,
}
impl ProjectType {
#[must_use]
pub fn detect() -> Self {
Self::detect_in(Path::new("."))
}
#[must_use]
pub fn detect_in(root: &Path) -> Self {
if root.join("Cargo.toml").exists() {
return Self::Rust;
}
if root.join("package.json").exists() {
return Self::Node;
}
if root.join("pyproject.toml").exists()
|| root.join("requirements.txt").exists()
|| root.join("Pipfile").exists()
{
return Self::Python;
}
if root.join("go.mod").exists() {
return Self::Go;
}
Self::Unknown
}
#[must_use]
pub fn is_typescript() -> bool {
Path::new("tsconfig.json").exists()
|| Path::new("tsconfig.node.json").exists()
|| has_ts_files()
}
}
fn has_ts_files() -> bool {
Path::new("src")
.read_dir()
.map(|entries| {
entries.flatten().any(|e| {
e.path()
.extension()
.is_some_and(|ext| ext == "ts" || ext == "tsx")
})
})
.unwrap_or(false)
}
#[must_use]
pub fn generate_toml(project: ProjectType, strictness: Strictness) -> String {
let rules = rules_section(strictness);
let commands = commands_section(project);
format!("# neti.toml\n{rules}\n\n{commands}\n")
}
fn rules_section(strictness: Strictness) -> String {
let (tokens, complexity, depth) = match strictness {
Strictness::Strict => (1500, 4, 2),
Strictness::Standard => (2000, 8, 3),
Strictness::Relaxed => (3000, 12, 4),
};
format!(
r#"[rules]
max_file_tokens = {tokens}
max_cyclomatic_complexity = {complexity}
max_nesting_depth = {depth}
max_function_args = 5
max_function_words = 5
ignore_naming_on = ["tests", "spec"]"#
)
}
fn commands_section(project: ProjectType) -> String {
match project {
ProjectType::Rust => make_commands(
r#"["cargo clippy --all-targets -- -D warnings -W clippy::pedantic", "cargo test"]"#,
r#""cargo fmt""#,
),
ProjectType::Node => {
let npx = npx_cmd();
if ProjectType::is_typescript() {
make_commands(
&format!(r#""{npx} @biomejs/biome check src/""#),
&format!(r#""{npx} @biomejs/biome check --write src/""#),
)
} else {
make_commands(
&format!(r#""{npx} eslint src/""#),
&format!(r#""{npx} eslint --fix src/""#),
)
}
}
ProjectType::Python => make_commands(r#""ruff check .""#, r#""ruff check --fix .""#),
ProjectType::Go => make_commands(r#""go vet ./...""#, r#""go fmt ./...""#),
ProjectType::Unknown => r#"# No project type detected. Configure commands manually:
# [commands]
# check = "your-lint-command"
# fix = "your-fix-command""#
.to_string(),
}
}
fn make_commands(check: &str, fix: &str) -> String {
format!("[commands]\ncheck = {check}\nfix = {fix}")
}
#[must_use]
pub fn npx_cmd() -> &'static str {
if cfg!(windows) {
"npx.cmd"
} else {
"npx"
}
}