use super::{documentation::documentation_line_flags, CodeContext};
use std::collections::BTreeSet;
use std::sync::LazyLock;
#[derive(serde::Deserialize)]
struct InferenceMarkers {
comment_prefixes: Vec<String>,
encrypted_prefixes: Vec<String>,
encrypted_substrings: Vec<String>,
rust_test_attribute_prefixes: Vec<String>,
rust_test_attribute_exact: Vec<String>,
test_function_prefixes: Vec<String>,
attribute_or_doc_prefixes: Vec<String>,
assignment_operators: Vec<String>,
}
const INFERENCE_MARKERS_TOML: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/rules/inference-markers.toml"
));
fn parse_inference_markers(raw: &str) -> Result<InferenceMarkers, String> {
toml::from_str(raw).map_err(|error| format!("invalid rules/inference-markers.toml: {error}"))
}
static INFERENCE_MARKERS: LazyLock<InferenceMarkers> =
LazyLock::new(|| match parse_inference_markers(INFERENCE_MARKERS_TOML) {
Ok(markers) => markers,
Err(error) => panic!(
"{error}. Fix the bundled Tier-B inference-marker file; refusing to run \
without credential-context classification truth."
),
});
const ENCRYPTED_BLOCK_LOOKBACK_LINES: usize = 10;
const TEST_FUNCTION_LOOKBACK_LINES: usize = 100;
const ATTR_BLOCK_LOOKBACK: usize = 32;
const CFG_TEST_ATTR: &str = concat!("#[cfg(", "test)]");
pub(crate) const COMMENT_MARKERS: &[&str] =
&["//", "#", "--", "/*", "<!--", "<#", "* ", "rem ", "REM "];
#[derive(serde::Deserialize)]
struct TestPathRuleFile {
schema_version: u32,
test_paths: TestPathRuleSection,
}
#[derive(serde::Deserialize)]
struct TestPathRuleSection {
filename_prefixes: Vec<String>,
filename_suffixes: Vec<String>,
path_components: Vec<String>,
}
#[derive(Debug)]
pub(crate) struct TestPathRules {
pub(crate) filename_prefixes: Vec<String>,
pub(crate) filename_suffixes: Vec<String>,
pub(crate) path_components: Vec<String>,
}
static TEST_PATH_RULES: LazyLock<TestPathRules> = LazyLock::new(|| {
match parse_test_path_rules(include_str!("../../data/test-path-rules.toml")) {
Ok(rules) => rules,
Err(error) => {
panic!(
"crates/scanner/data/test-path-rules.toml is invalid: {error}. \
Fix the bundled Tier-B test-path rules; refusing to run without \
test-context classification truth."
)
}
}
});
pub fn infer_context(lines: &[&str], line_idx: usize, file_path: Option<&str>) -> CodeContext {
let documentation_lines = documentation_line_flags(lines);
infer_context_with_documentation(lines, line_idx, file_path, &documentation_lines)
}
fn is_encrypted_marker_line(trimmed: &str) -> bool {
INFERENCE_MARKERS
.encrypted_prefixes
.iter()
.any(|prefix| trimmed.starts_with(prefix.as_str()))
|| INFERENCE_MARKERS
.encrypted_substrings
.iter()
.any(|sub| memchr::memmem::find(trimmed.as_bytes(), sub.as_bytes()).is_some())
}
pub(crate) fn infer_context_with_documentation(
lines: &[&str],
line_idx: usize,
file_path: Option<&str>,
documentation_lines: &[bool],
) -> CodeContext {
if line_idx >= lines.len() {
return CodeContext::Unknown;
}
let line = lines[line_idx];
let trimmed = line.trim();
if file_path.is_some_and(is_test_file) {
return CodeContext::TestCode;
}
if is_in_encrypted_block(lines, line_idx) {
return CodeContext::Encrypted;
}
if is_commented_assignment_line(trimmed) {
return CodeContext::Assignment;
}
if is_comment_line(trimmed) {
return CodeContext::Comment;
}
if documentation_lines
.get(line_idx)
.copied()
.is_some_and(|v| v)
{
return CodeContext::Documentation;
}
if is_in_test_function(lines, line_idx) {
return CodeContext::TestCode;
}
if is_assignment_line(trimmed) {
return CodeContext::Assignment;
}
infer_default_context(trimmed)
}
fn is_test_file(path: &str) -> bool {
let rules = test_path_rules();
let filename = crate::platform_compat::path_basename(path);
let stem = filename.split('.').next().unwrap_or(filename);
rules.filename_prefixes.iter().any(|prefix| {
stem.len() >= prefix.len()
&& stem
.as_bytes()
.get(..prefix.len())
.is_some_and(|bytes| bytes.eq_ignore_ascii_case(prefix.as_bytes()))
}) || rules
.filename_suffixes
.iter()
.any(|suffix| filename.ends_with(suffix))
|| crate::platform_compat::path_has_any_component(path, &rules.path_components)
}
fn test_path_rules() -> &'static TestPathRules {
&TEST_PATH_RULES
}
pub(crate) fn parse_test_path_rules(raw: &str) -> Result<TestPathRules, String> {
let parsed: TestPathRuleFile =
toml::from_str(raw).map_err(|error| format!("invalid test-path-rules.toml: {error}"))?;
if parsed.schema_version != 1 {
return Err(format!(
"unsupported test-path-rules schema_version {}",
parsed.schema_version
));
}
Ok(TestPathRules {
filename_prefixes: validate_rule_list(
"test_paths.filename_prefixes",
parsed.test_paths.filename_prefixes,
RuleListKind::FilenameFragment,
)?,
filename_suffixes: validate_rule_list(
"test_paths.filename_suffixes",
parsed.test_paths.filename_suffixes,
RuleListKind::FilenameFragment,
)?,
path_components: validate_rule_list(
"test_paths.path_components",
parsed.test_paths.path_components,
RuleListKind::PathComponent,
)?,
})
}
#[derive(Clone, Copy)]
enum RuleListKind {
FilenameFragment,
PathComponent,
}
fn validate_rule_list(
field: &str,
values: Vec<String>,
kind: RuleListKind,
) -> Result<Vec<String>, String> {
if values.is_empty() {
return Err(format!("{field} must contain at least one entry"));
}
let mut seen = BTreeSet::new();
let mut out = Vec::with_capacity(values.len());
for raw in values {
let value = raw.trim();
if value.is_empty() {
return Err(format!("{field} entries must not be empty"));
}
if value.bytes().any(|byte| byte == b'/' || byte == b'\\') {
return Err(format!(
"{field} entry {value:?} must not contain path separators"
));
}
if matches!(kind, RuleListKind::PathComponent) && value.contains('.') {
return Err(format!(
"{field} component {value:?} must be a path segment, not a filename pattern"
));
}
if !seen.insert(value.to_string()) {
return Err(format!("duplicate {field} entry {value:?}"));
}
out.push(value.to_string());
}
Ok(out)
}
fn infer_default_context(trimmed: &str) -> CodeContext {
if memchr::memchr(b'"', trimmed.as_bytes()).is_some()
|| memchr::memchr(b'\'', trimmed.as_bytes()).is_some()
{
CodeContext::StringLiteral
} else {
CodeContext::Unknown
}
}
fn is_comment_line(trimmed: &str) -> bool {
(trimmed.starts_with("--") && !trimmed.starts_with("---"))
|| INFERENCE_MARKERS
.comment_prefixes
.iter()
.any(|prefix| trimmed.starts_with(prefix.as_str()))
}
fn is_commented_assignment_line(trimmed: &str) -> bool {
let Some(comment_body) = strip_comment_prefix(trimmed) else {
return false;
};
let body = comment_body
.trim_start()
.trim_end_matches("*/")
.trim_end_matches("-->")
.trim();
has_assignment_operator(body) || has_yaml_mapping(body)
}
pub(crate) fn strip_comment_prefix(trimmed: &str) -> Option<&str> {
for &marker in COMMENT_MARKERS {
if marker == "--" && trimmed.starts_with("---") {
continue;
}
if let Some(rest) = trimmed.strip_prefix(marker) {
return Some(rest);
}
}
None
}
fn is_assignment_line(trimmed: &str) -> bool {
has_assignment_operator(trimmed) || has_yaml_mapping(trimmed)
}
pub(crate) fn has_assignment_operator(trimmed: &str) -> bool {
for operator in &INFERENCE_MARKERS.assignment_operators {
let op_str = operator.as_str();
if let Some(pos) = trimmed.find(op_str) {
if !is_comparison_operator(trimmed, pos, op_str) {
return true;
}
}
}
false
}
fn has_yaml_mapping(trimmed: &str) -> bool {
memchr::memmem::find(trimmed.as_bytes(), b": ").is_some() && !trimmed.starts_with("- ")
}
fn is_comparison_operator(trimmed: &str, pos: usize, operator: &str) -> bool {
if operator != "=" {
return false;
}
let before = trimmed[..pos].chars().last();
let after = trimmed[pos + operator.len()..].chars().next();
matches!(before, Some('=' | '!' | '>' | '<')) || matches!(after, Some('='))
}
fn is_in_encrypted_block(lines: &[&str], line_idx: usize) -> bool {
let end = (line_idx + 1).min(lines.len());
let start = line_idx
.saturating_sub(ENCRYPTED_BLOCK_LOOKBACK_LINES)
.min(end);
lines[start..end]
.iter()
.any(|line| is_encrypted_marker_line(line.trim()))
}
pub(crate) fn is_in_test_function(lines: &[&str], line_idx: usize) -> bool {
let start = line_idx.saturating_sub(TEST_FUNCTION_LOOKBACK_LINES);
for candidate_line_idx in (start..line_idx).rev() {
let trimmed = lines[candidate_line_idx].trim();
if INFERENCE_MARKERS
.test_function_prefixes
.iter()
.any(|prefix| trimmed.starts_with(prefix.as_str()))
|| is_rust_test_attribute(trimmed)
{
return true;
}
if trimmed.starts_with("class ") {
return false;
}
if (trimmed.starts_with("def ") || trimmed.starts_with("async def "))
&& !trimmed.contains("def test_")
{
return false;
}
if trimmed.starts_with("func ") && !trimmed.contains("func Test") {
return false;
}
if is_rust_fn_signature(trimmed) && !trimmed.contains("fn test_") {
let block_start = candidate_line_idx.saturating_sub(ATTR_BLOCK_LOOKBACK);
for pre_line in lines[block_start..candidate_line_idx].iter().rev() {
let pre_trimmed = pre_line.trim();
if pre_trimmed.is_empty() {
continue;
}
if is_rust_test_attribute(pre_trimmed) {
return true;
}
if !is_attribute_or_doc_line(pre_trimmed) {
break;
}
}
return false;
}
if trimmed.starts_with("function ") && !trimmed.contains("function test") {
return false;
}
}
false
}
pub(crate) fn is_rust_fn_signature(trimmed: &str) -> bool {
let mut rest = trimmed;
loop {
if let Some(after) = rest.strip_prefix("pub") {
let after = after.trim_start();
if let Some(paren) = after.strip_prefix('(') {
match paren.find(')') {
Some(close) => {
rest = paren[close + 1..].trim_start();
continue;
}
None => return false,
}
}
rest = after;
continue;
}
if let Some(after) = rest.strip_prefix("extern") {
let after = after.trim_start();
if let Some(quoted) = after.strip_prefix('"') {
match quoted.find('"') {
Some(close) => {
rest = quoted[close + 1..].trim_start();
continue;
}
None => return false,
}
}
rest = after;
continue;
}
let stripped = ["const ", "unsafe ", "async ", "default "]
.iter()
.find_map(|kw| rest.strip_prefix(kw));
match stripped {
Some(after) => rest = after.trim_start(),
None => break,
}
}
rest.starts_with("fn ")
}
fn is_rust_test_attribute(trimmed: &str) -> bool {
trimmed == CFG_TEST_ATTR
|| INFERENCE_MARKERS
.rust_test_attribute_exact
.iter()
.any(|attr| trimmed == attr.as_str())
|| INFERENCE_MARKERS
.rust_test_attribute_prefixes
.iter()
.any(|prefix| trimmed.starts_with(prefix.as_str()))
}
fn is_attribute_or_doc_line(trimmed: &str) -> bool {
trimmed.ends_with("*/")
|| INFERENCE_MARKERS
.attribute_or_doc_prefixes
.iter()
.any(|prefix| trimmed.starts_with(prefix.as_str()))
}
pub(crate) fn surrounding_line_window(text: &str, offset: usize, radius: usize) -> &str {
if text.is_empty() {
return "";
}
let bytes = text.as_bytes();
let safe_offset = offset.min(bytes.len());
const FP_HEURISTIC_WINDOW_BYTES: usize = 2 * 1024;
let mut start = safe_offset;
let mut found_lines = 0;
while start > 0 && found_lines <= radius && safe_offset - start < FP_HEURISTIC_WINDOW_BYTES {
start -= 1;
if bytes[start] == b'\n' {
found_lines += 1;
}
}
if start > 0 || (start == 0 && bytes[0] == b'\n') {
start += 1;
}
let mut end = safe_offset;
let mut found_lines = 0;
while end < bytes.len()
&& found_lines <= radius
&& end - safe_offset < FP_HEURISTIC_WINDOW_BYTES
{
if bytes[end] == b'\n' {
found_lines += 1;
}
end += 1;
}
let start = crate::engine::ceil_char_boundary(text, start);
let mut end = crate::engine::floor_char_boundary(text, end);
if end < start {
end = start;
}
&text[start..end]
}