Skip to content
Merged

4 #7

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
schemars = "1"
clap = { version = "4", features = ["derive"] }
regex = "1"
toml = "1"

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
15 changes: 15 additions & 0 deletions crates/rustmanifest-rules-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,24 @@ authors.workspace = true
readme = "README.md"
categories = ["development-tools"]
keywords = ["rust", "lint", "code-review", "rules"]
build = "build.rs"
include = [
"src/**/*",
"rules/**/*",
"build.rs",
"Cargo.toml",
"README.md",
]

[dependencies]
regex = { workspace = true }
rustmanifest-schema = { workspace = true }
serde_json = { workspace = true }

[build-dependencies]
rustmanifest-schema = { workspace = true }
serde_json = { workspace = true }
toml = { workspace = true }

[lints]
workspace = true
90 changes: 90 additions & 0 deletions crates/rustmanifest-rules-core/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

//! Build script that bundles every rule defined under `rules/` into a single
//! `rules.json` artifact emitted in `OUT_DIR`. The runtime crate
//! `include_str!`s the result and parses it once on first access through a
//! `LazyLock`.

use std::{
env, fs,
io::{self, Write},
path::{Path, PathBuf}
};

use rustmanifest_schema::Rule;

type BuildError = Box<dyn std::error::Error>;

fn main() -> Result<(), BuildError> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let rules_dir = manifest_dir.join("rules");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);

emit_rerun(&rules_dir)?;

let rules = collect_rules(&rules_dir)?;
let json = serde_json::to_string_pretty(&rules)?;
fs::write(out_dir.join("rules.json"), json)?;

Ok(())
}

fn collect_rules(rules_dir: &Path) -> Result<Vec<Rule>, BuildError> {
let mut entries: Vec<_> = fs::read_dir(rules_dir)?
.filter_map(Result::ok)
.filter(|entry| entry.path().is_dir())
.collect();
entries.sort_by_key(std::fs::DirEntry::path);

let mut rules = Vec::with_capacity(entries.len());
for entry in entries {
rules.push(parse_one(&entry.path())?);
}
Ok(rules)
}

fn parse_one(dir: &Path) -> Result<Rule, BuildError> {
let dir_name_os = dir
.file_name()
.ok_or_else(|| format!("cannot read file name of {}", dir.display()))?;
let dir_name = dir_name_os
.to_str()
.ok_or_else(|| format!("non-utf8 directory name at {}", dir.display()))?;

let rule_toml = dir.join("rule.toml");
let pass_rs = dir.join("pass.rs");
let fail_rs = dir.join("fail.rs");

if !rule_toml.is_file() {
return Err(format!("missing rule.toml in {}", dir.display()).into());
}
if !pass_rs.is_file() {
return Err(format!("missing pass.rs in {}", dir.display()).into());
}
if !fail_rs.is_file() {
return Err(format!("missing fail.rs in {}", dir.display()).into());
}

let text = fs::read_to_string(&rule_toml)?;
let rule: Rule = toml::from_str(&text)?;

if rule.id != dir_name {
return Err(format!(
"rule id {:?} does not match directory name {:?} in {}",
rule.id,
dir_name,
dir.display()
)
.into());
}

Ok(rule)
}

fn emit_rerun(rules_dir: &Path) -> io::Result<()> {
let mut stdout = io::stdout().lock();
writeln!(stdout, "cargo::rerun-if-changed={}", rules_dir.display())?;
writeln!(stdout, "cargo::rerun-if-changed=build.rs")?;
Ok(())
}
10 changes: 10 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-PERF-001/fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

fn build_ids(count: usize) -> Vec<u64> {
let mut ids: Vec<u64> = Vec::new();
for i in 0..count {
ids.push(i as u64);
}
ids
}
10 changes: 10 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-PERF-001/pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

fn build_ids(count: usize) -> Vec<u64> {
let mut ids: Vec<u64> = Vec::with_capacity(count);
for i in 0..count {
ids.push(i as u64);
}
ids
}
11 changes: 11 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-PERF-001/rule.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
# SPDX-License-Identifier: MIT

id = "RM-PERF-001"
severity = "warning"
title = "Vec::new() without with_capacity"
rationale-uri = "rustmanifest://methodology/performance-issues#vec-without-capacity"

[definition.pattern]
regex = "Vec::new\\(\\)"
exclude-globs = ["**/tests/**", "**/*_test.rs", "**/test_*.rs"]
10 changes: 10 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-RUST-001/fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

fn parse_port(raw: &str) -> u16 {
raw.parse().unwrap()
}

fn must_have(name: &str) -> String {
std::env::var(name).expect("env var missing")
}
13 changes: 13 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-RUST-001/pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

#[derive(Debug)]
struct ConfigError;

fn parse_port(raw: &str) -> Result<u16, ConfigError> {
raw.parse().map_err(|_| ConfigError)
}

fn must_have(name: &str) -> Result<String, ConfigError> {
std::env::var(name).map_err(|_| ConfigError)
}
11 changes: 11 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-RUST-001/rule.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
# SPDX-License-Identifier: MIT

id = "RM-RUST-001"
severity = "error"
title = "Panic in production code"
rationale-uri = "rustmanifest://methodology/rust-specific#panic-in-production"

[definition.pattern]
regex = "\\.unwrap\\(\\)|\\.expect\\(|panic!"
exclude-globs = ["**/tests/**", "**/*_test.rs", "**/test_*.rs", "**/benches/**"]
7 changes: 7 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-SEC-001/fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

fn main() {
let password = "hunter2";
let api_key = "sk-abc123";
}
10 changes: 10 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-SEC-001/pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
// SPDX-License-Identifier: MIT

fn validate_password(input: &str) -> bool {
!input.is_empty()
}

fn load_api_key_from_env() -> Option<String> {
std::env::var("API_KEY").ok()
}
11 changes: 11 additions & 0 deletions crates/rustmanifest-rules-core/rules/RM-SEC-001/rule.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 RAprogramm <andrey.rozanov.vl@gmail.com>
# SPDX-License-Identifier: MIT

id = "RM-SEC-001"
severity = "error"
title = "Hardcoded credentials"
rationale-uri = "rustmanifest://methodology/security-vulnerabilities#hardcoded-credentials"

[definition.pattern]
regex = "(?i)(password|secret|api_key|token|private_key)\\s*=\\s*[\"']"
exclude-globs = ["**/tests/**", "**/*_test.rs", "**/test_*.rs"]
Loading
Loading