use super::types::{EnvDeclaration, EnvSource, EnvSourceKind, EnvWarning, ValuePresence};
pub const DEFAULT_STALE_THRESHOLD_DAYS: u32 = 7;
pub fn compute_warnings(
decl: &mut EnvDeclaration,
has_env_contracts: bool,
stale_threshold_days: u32,
) {
decl.precedence_warnings.clear();
if decl.sources.is_empty() && !decl.reads.is_empty() && !is_runtime_provided_env(&decl.name) {
decl.precedence_warnings
.push(EnvWarning::OrphanCodeReference {
read_sites: decl.reads.iter().map(|r| r.file.clone()).collect(),
});
}
let plain_sources: Vec<&EnvSource> = decl
.sources
.iter()
.filter(|s| matches!(s.value_present, ValuePresence::Plain { .. }))
.collect();
if plain_sources.len() >= 2 {
let first_hash = plain_value_hash(plain_sources[0]);
let mismatch = plain_sources
.iter()
.any(|s| plain_value_hash(s) != first_hash);
if mismatch {
decl.precedence_warnings
.push(EnvWarning::MultiSourceValueMismatch {
sources: plain_sources.iter().map(|s| s.path.clone()).collect(),
});
}
}
if has_env_contracts && decl.reads.is_empty() && !is_synthetic(&decl.name) {
decl.precedence_warnings
.push(EnvWarning::OrphanDeclaration {
sources: decl.sources.iter().map(|s| s.path.clone()).collect(),
});
}
if decl.sources.len() >= 2 {
let high = &decl.sources[0];
let low = &decl.sources[1];
if let (Some(h_age), Some(l_age)) = (high.mtime_age_days, low.mtime_age_days)
&& h_age > l_age
&& h_age.saturating_sub(l_age) >= stale_threshold_days
{
let delta = h_age.saturating_sub(l_age);
decl.precedence_warnings
.push(EnvWarning::StaleOverridesFresh {
stale_source: high.path.clone(),
fresh_source: low.path.clone(),
age_delta_days: delta,
});
}
if matches!(
high.kind,
EnvSourceKind::SealedSecret | EnvSourceKind::SopsFile | EnvSourceKind::ExternalSecret
) && let Some(h_age) = high.mtime_age_days
{
let plain_fresh_neighbor = decl.sources[1..].iter().find(|s| {
matches!(s.value_present, ValuePresence::Plain { .. })
&& s.mtime_age_days
.is_some_and(|a| h_age > a && h_age - a >= stale_threshold_days)
});
if let Some(neighbor) = plain_fresh_neighbor {
decl.precedence_warnings
.push(EnvWarning::SealedSecretSuspectedStale {
sealed_path: high.path.clone(),
sealed_age_days: h_age,
plain_age_days: neighbor.mtime_age_days.unwrap_or(0),
});
}
}
}
for src in &decl.sources {
if matches!(src.value_present, ValuePresence::Encrypted { .. }) {
decl.precedence_warnings
.push(EnvWarning::EncryptedDecodeBlocked {
source: src.path.clone(),
});
}
}
}
const OS_STANDARD_ENV: &[&str] = &[
"HOME",
"PATH",
"LANG",
"LANGUAGE",
"USER",
"LOGNAME",
"SHELL",
"TERM",
"PWD",
"OLDPWD",
"TMPDIR",
"TMP",
"TEMP",
"HOSTNAME",
"EDITOR",
"VISUAL",
"PAGER",
"DISPLAY",
"COLORTERM",
];
pub(super) fn is_runtime_provided_env(name: &str) -> bool {
OS_STANDARD_ENV.contains(&name)
|| name.starts_with("LC_")
|| name.starts_with("XDG_")
|| crate::semantic::shell::is_shell_runtime_var(name)
}
fn plain_value_hash(s: &EnvSource) -> Option<&str> {
match &s.value_present {
ValuePresence::Plain { value_hash } => Some(value_hash.as_str()),
_ => None,
}
}
fn is_synthetic(name: &str) -> bool {
name.starts_with("__") && name.ends_with("__")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pack::AuthorityLabel;
fn mk_source(
kind: EnvSourceKind,
path: &str,
rank: u8,
age_days: u32,
value: ValuePresence,
) -> EnvSource {
EnvSource {
kind,
path: path.into(),
line: None,
mtime: "2026-01-01T00:00:00Z".into(),
mtime_age_days: Some(age_days),
git_age_days: None,
value_present: value,
precedence_rank: rank,
}
}
fn decl_from_sources(sources: Vec<EnvSource>) -> EnvDeclaration {
EnvDeclaration {
name: "TEST".into(),
sources,
reads: vec![],
precedence_warnings: vec![],
authority: AuthorityLabel::SemanticGuess,
}
}
#[test]
fn detects_multi_source_mismatch_via_hashes() {
let mut decl = decl_from_sources(vec![
mk_source(
EnvSourceKind::DotEnv,
".env",
30,
1,
ValuePresence::Plain {
value_hash: "aaaaaaaaaaaa".into(),
},
),
mk_source(
EnvSourceKind::DockerCompose,
"docker-compose.yml",
50,
1,
ValuePresence::Plain {
value_hash: "bbbbbbbbbbbb".into(),
},
),
]);
compute_warnings(&mut decl, false, 7);
assert!(
decl.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::MultiSourceValueMismatch { .. }))
);
}
#[test]
fn detects_sealed_overrides_fresh_plain() {
let mut decl = decl_from_sources(vec![
mk_source(
EnvSourceKind::SealedSecret,
"k8s/sealed.yaml",
100,
30,
ValuePresence::Encrypted {
marker: "SealedSecret".into(),
},
),
mk_source(
EnvSourceKind::DotEnv,
".env",
30,
2,
ValuePresence::Plain {
value_hash: "cafe".repeat(3),
},
),
]);
compute_warnings(&mut decl, false, 7);
assert!(
decl.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::SealedSecretSuspectedStale { .. }))
);
assert!(
decl.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::StaleOverridesFresh { .. }))
);
}
#[test]
fn no_stale_warning_when_within_threshold() {
let mut decl = decl_from_sources(vec![
mk_source(
EnvSourceKind::SealedSecret,
"k8s/sealed.yaml",
100,
3,
ValuePresence::Encrypted {
marker: "SealedSecret".into(),
},
),
mk_source(
EnvSourceKind::DotEnv,
".env",
30,
1,
ValuePresence::Plain {
value_hash: "1234".repeat(3),
},
),
]);
compute_warnings(&mut decl, false, 7);
assert!(
!decl
.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::StaleOverridesFresh { .. }))
);
}
#[test]
fn runtime_provided_reads_are_not_orphan_code_references() {
use crate::analyzer::env_truth::types::EnvReadSite;
for name in ["HOME", "PATH", "LC_ALL", "XDG_CONFIG_HOME", "GITHUB_ENV"] {
let mut decl = decl_from_sources(vec![]);
decl.name = name.into();
decl.reads = vec![EnvReadSite {
file: "src/lib.rs".into(),
line: None,
symbol: None,
required_for: vec![],
}];
compute_warnings(&mut decl, true, 7);
assert!(
!decl
.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::OrphanCodeReference { .. })),
"runtime-provided `{name}` must not be an orphan code reference"
);
}
let mut decl = decl_from_sources(vec![]);
decl.name = "MY_API_KEY".into();
decl.reads = vec![EnvReadSite {
file: "src/lib.rs".into(),
line: None,
symbol: None,
required_for: vec![],
}];
compute_warnings(&mut decl, true, 7);
assert!(
decl.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::OrphanCodeReference { .. }))
);
}
#[test]
fn orphan_declaration_only_when_env_contracts_present() {
let mut decl = decl_from_sources(vec![mk_source(
EnvSourceKind::DotEnv,
".env",
30,
1,
ValuePresence::Plain {
value_hash: "abcd".repeat(3),
},
)]);
compute_warnings(&mut decl, false, 7);
assert!(
!decl
.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::OrphanDeclaration { .. }))
);
let mut decl2 = decl.clone();
decl2.precedence_warnings.clear();
compute_warnings(&mut decl2, true, 7);
assert!(
decl2
.precedence_warnings
.iter()
.any(|w| matches!(w, EnvWarning::OrphanDeclaration { .. }))
);
}
}