pub struct Scan {
pub depth: i32,
pub unterminated: bool,
pub code: Vec<bool>,
}
pub fn scan(s: &str) -> Scan {
let b = s.as_bytes();
let mut code = vec![false; b.len()];
let mut depth = 0i32;
let mut i = 0;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut in_str = false;
let mut in_chr = false;
while i < b.len() {
let c = b[i];
let next = b.get(i + 1).copied();
if in_line_comment {
if c == b'\n' {
in_line_comment = false;
}
i += 1;
continue;
}
if in_block_comment {
if c == b'*' && next == Some(b'/') {
in_block_comment = false;
i += 2;
continue;
}
i += 1;
continue;
}
if in_str || in_chr {
if c == b'\\' {
i += 2;
continue;
}
if (in_str && c == b'"') || (in_chr && c == b'\'') {
in_str = false;
in_chr = false;
}
i += 1;
continue;
}
match c {
b'/' if next == Some(b'/') => {
in_line_comment = true;
i += 2;
continue;
}
b'/' if next == Some(b'*') => {
in_block_comment = true;
i += 2;
continue;
}
b'"' => in_str = true,
b'\'' => in_chr = true,
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
_ => {}
}
code[i] = true;
i += 1;
}
Scan {
depth,
unterminated: in_str || in_chr || in_block_comment,
code,
}
}
fn is_ident_byte(c: u8) -> bool {
c == b'_' || c.is_ascii_alphanumeric()
}
pub fn contains_code_identifier(src: &str, wanted: &str) -> bool {
let sc = scan(src);
let bytes = src.as_bytes();
let mut i = 0;
while i < bytes.len() {
if sc.code[i] && (bytes[i] == b'_' || bytes[i].is_ascii_alphabetic()) {
let start = i;
i += 1;
while i < bytes.len() && sc.code[i] && is_ident_byte(bytes[i]) {
i += 1;
}
if &src[start..i] == wanted {
return true;
}
} else {
i += 1;
}
}
false
}
pub fn identifiers(src: &str) -> Vec<String> {
let sc = scan(src);
let b = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < b.len() {
if sc.code[i] && is_ident_byte(b[i]) {
let start = i;
while i < b.len() && sc.code[i] && is_ident_byte(b[i]) {
i += 1;
}
if !b[start].is_ascii_digit() {
out.push(src[start..i].to_string());
}
} else {
i += 1;
}
}
out
}
pub fn is_blank(src: &str) -> bool {
let sc = scan(src);
src.bytes()
.zip(sc.code)
.all(|(b, is_code)| !is_code || b.is_ascii_whitespace())
}
pub fn may_have_side_effects(src: &str) -> bool {
const COMPARISONS: [u8; 4] = *b"=!<>";
const PURE_CALLS: [&str; 3] = ["sizeof", "_Alignof", "alignof"];
let sc = scan(src);
let b = src.as_bytes();
let mut prev: u8 = 0;
let mut prev_word: &str = "";
let mut i = 0;
while i < b.len() {
if !sc.code[i] || b[i].is_ascii_whitespace() {
i += 1;
continue;
}
let c = b[i];
match c {
b'=' => {
let prev_raw = i.checked_sub(1).map(|p| b[p]);
let prev2_raw = i.checked_sub(2).map(|p| b[p]);
let shift_assign =
prev_raw.is_some_and(|p| p == b'<' || p == b'>') && prev2_raw == prev_raw;
let is_comparison = !shift_assign
&& (prev_raw.is_some_and(|p| COMPARISONS.contains(&p))
|| b.get(i + 1) == Some(&b'='));
if !is_comparison {
return true;
}
}
b'+' | b'-' if b.get(i + 1) == Some(&c) => return true,
b'(' => {
if prev == b')' || prev == b']' {
return true;
}
if is_ident_byte(prev) && !PURE_CALLS.contains(&prev_word) {
return true;
}
}
_ => {}
}
if is_ident_byte(c) {
let start = i;
while i < b.len() && sc.code[i] && is_ident_byte(b[i]) {
i += 1;
}
prev_word = &src[start..i];
prev = b[i - 1];
continue;
}
prev = c;
i += 1;
}
false
}
pub fn external_replay_calls(src: &str) -> Vec<String> {
const RISKY: &[&str] = &[
"fopen",
"freopen",
"fclose",
"fflush",
"fprintf",
"fscanf",
"fgetc",
"fgets",
"fputc",
"fputs",
"fread",
"fwrite",
"scanf",
"getchar",
"remove",
"rename",
"tmpfile",
"tmpnam",
"system",
"popen",
"pclose",
"open",
"creat",
"close",
"read",
"write",
"unlink",
"mkdir",
"rmdir",
"chdir",
"chmod",
"truncate",
"ftruncate",
"fsync",
"fork",
"execl",
"execle",
"execlp",
"execv",
"execve",
"execvp",
"kill",
"setenv",
"unsetenv",
"putenv",
];
let sc = scan(src);
let b = src.as_bytes();
let mut found = Vec::new();
let mut i = 0;
while i < b.len() {
if !sc.code[i] || !(b[i] == b'_' || b[i].is_ascii_alphabetic()) {
i += 1;
continue;
}
let start = i;
i += 1;
while i < b.len() && sc.code[i] && is_ident_byte(b[i]) {
i += 1;
}
let word = &src[start..i];
if !RISKY.contains(&word) || found.iter().any(|seen| seen == word) {
continue;
}
let mut next = i;
while next < b.len() && (!sc.code[next] || b[next].is_ascii_whitespace()) {
next += 1;
}
if b.get(next) == Some(&b'(') {
found.push(word.to_string());
}
}
found
}
pub fn awaits_body(input: &str) -> bool {
const NOT_DECLARATOR: &[&str] = &[
"return", "sizeof", "if", "while", "for", "switch", "case", "goto", "do", "else",
];
let sc = scan(input);
if sc.depth != 0 || sc.unterminated {
return false;
}
let t = input.trim_end();
if !t.ends_with(')') {
return false;
}
let b = t.as_bytes();
let Some(open) = (0..b.len()).find(|&i| sc.code[i] && b[i] == b'(') else {
return false;
};
let head = &t[..open];
if !head
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '*' || c.is_whitespace())
{
return false;
}
let words: Vec<&str> = head
.split(|c: char| c.is_whitespace() || c == '*')
.filter(|s| !s.is_empty())
.collect();
words.len() >= 2 && !NOT_DECLARATOR.contains(&words[0])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_external_replay_risks_without_reading_comments_or_literals() {
assert_eq!(
external_replay_calls(
"FILE *fp = fopen(\"data\", \"a\");\n\
fprintf /* comment */ (fp, \"line\\n\"); fclose(fp); fopen(\"x\", \"r\");"
),
vec!["fopen", "fprintf", "fclose"]
);
assert!(external_replay_calls("// remove(\"x\")\n\"system(\"").is_empty());
assert_eq!(
external_replay_calls("system (\"echo hi\")"),
vec!["system"]
);
}
#[test]
fn detects_a_signature_waiting_for_its_body() {
assert!(awaits_body("int fact(int n)"));
assert!(awaits_body("static unsigned long g(void)"));
assert!(awaits_body("int *make(int n)"));
assert!(awaits_body("int*make(int n)"));
}
#[test]
fn leaves_finished_input_alone() {
assert!(!awaits_body("fact(5)"));
assert!(!awaits_body("printf(\"hi\")"));
assert!(!awaits_body("return fact(5)"));
assert!(!awaits_body("x = fact(5)"));
assert!(!awaits_body("int fact(int n);"));
assert!(!awaits_body("int fact(int n) { return 1; }"));
}
#[test]
fn depth_ignores_brackets_in_literals() {
assert_eq!(scan("f(\"(((\")").depth, 0);
assert_eq!(scan("x = '{';").depth, 0);
assert_eq!(scan("void f(void) {").depth, 1);
}
#[test]
fn detects_unterminated() {
assert!(scan("\"abc").unterminated);
assert!(scan("/* open").unterminated);
assert!(!scan("\"a\\\"b\"").unterminated);
}
#[test]
fn exact_code_identifier_search_ignores_comments_and_literals() {
assert!(contains_code_identifier("scanf(\"%d\", &x)", "scanf"));
assert!(!contains_code_identifier("my_scanf()", "scanf"));
assert!(!contains_code_identifier("// scanf()\n\"scanf\"", "scanf"));
}
#[test]
fn harvests_code_identifiers_only() {
assert_eq!(
identifiers("int x = f(y); // z"),
vec!["int", "x", "f", "y"]
);
assert_eq!(identifiers("puts(\"not_this\")"), vec!["puts"]);
assert!(identifiers("42 + 0x1F").is_empty());
}
#[test]
fn blank_means_comments_and_whitespace_only() {
assert!(is_blank("// just a note"));
assert!(is_blank("/* block */"));
assert!(is_blank(" "));
assert!(!is_blank("x // trailing"));
assert!(!is_blank("\"text\""));
}
#[test]
fn treats_questions_as_pure() {
assert!(!may_have_side_effects("x + 1"));
assert!(!may_have_side_effects("3.0 / 2"));
assert!(!may_have_side_effects("sizeof(int)"));
assert!(!may_have_side_effects("sizeof (arr) / sizeof (arr[0])"));
assert!(!may_have_side_effects("a == b"));
assert!(!may_have_side_effects("a != b && c <= d"));
assert!(!may_have_side_effects("-1 > 0u"));
assert!(!may_have_side_effects("\"a = b\""));
}
#[test]
fn treats_mutation_and_calls_as_impure() {
assert!(may_have_side_effects("x = 5"));
assert!(may_have_side_effects("x += 5"));
assert!(may_have_side_effects("x <<= 1"));
assert!(may_have_side_effects("x >>= 1"));
assert!(may_have_side_effects("x++"));
assert!(may_have_side_effects("--x"));
assert!(may_have_side_effects("puts(\"hi\")"));
assert!(may_have_side_effects("fact(5)"));
}
#[test]
fn detects_calls_hidden_by_comments_or_parens() {
assert!(may_have_side_effects("f/**/()"));
assert!(may_have_side_effects("f /* comment */ ()"));
assert!(may_have_side_effects("(f)()"));
assert!(may_have_side_effects("(*fp)()"));
assert!(may_have_side_effects("arr[0]()"));
}
#[test]
fn undecidable_forms_are_kept_not_dropped() {
assert!(may_have_side_effects("(int)(x)"));
assert!(!may_have_side_effects("x +/**/+ y"));
}
}