aurora-lint 0.5.2

aurora-lint - a fast CERT C static analyzer
use super::function_summary::FunctionSummary;
use super::macro_expand::FunctionMacro;
use super::null_state::NullState;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;

/// Cross-file context gathered by pre-scanning additional directories.
///
/// Holds function names found in `.c`/`.h` files so that rules like DCL31-C
/// and DCL07-C can suppress false positives for project-internal functions
/// defined in other translation units.
///
/// The tables are `Arc`-wrapped because every scanned file hands this context
/// to a fresh set of rule instances, each of which keeps its own handle
/// (`set_project_context`). A handle is a refcount bump; a deep copy of the
/// function summaries of a few-thousand-file project, per rule, per file, was
/// the dominant cost of a scan. Build the tables in full, then wrap; after
/// that, mutate only through `Arc::make_mut` and only before rules see the
/// context (`resolve_includes`, the compile-database merge).
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectContext {
    /// Every function name found in the pre-scanned `.c`/`.h` files.
    pub known_functions: Arc<HashSet<String>>,
    /// Functions declared (prototyped) in `.h` header files.
    /// A function with a header prototype is public API and should not be
    /// flagged by DCL15-C/DCL19-C as needing `static`.
    pub header_declared_functions: Arc<HashSet<String>>,
    /// Function summaries computed during prescan for inter-procedural analysis.
    pub function_summaries: Arc<HashMap<String, FunctionSummary>>,
    /// Call graph: maps function name to the set of functions it calls.
    pub call_graph: Arc<HashMap<String, HashSet<String>>>,
    /// The inverse of `call_graph`: maps a function name to the set of
    /// functions that call it. Computed once when the context is built, so
    /// a rule asking "who calls this?" per file does not re-invert the whole
    /// graph per file (six rules did, each on every file).
    #[serde(default)]
    pub callers: Arc<HashMap<String, HashSet<String>>>,
    /// Callee names that must never be resolved to a same-named function
    /// definition by name matching alone: names reached only through a
    /// `field_expression` call (`obj->cb(...)`) or through a plain
    /// identifier that is also a parameter name of the calling function
    /// (a callback passed by the caller, shadowing any same-named global
    /// function per C scoping rules). `call_graph` may still contain edges
    /// to these names (recorded by the underlying, name-matching-only call
    /// graph builder), so a consumer doing cycle/reachability analysis
    /// through unresolved indirect calls should treat any callee in this
    /// set as opaque rather than chase it (task 562).
    #[serde(default)]
    pub ambiguous_call_targets: Arc<HashSet<String>>,
    /// Macro constants collected from `#define` directives across all scanned files.
    pub macro_constants: Arc<HashMap<String, i64>>,
    /// Macro aliases: `#define ALIAS identifier` patterns (e.g., `SYSTEM` → `system`).
    /// Used by rules to resolve function calls through macro indirection.
    pub macro_aliases: Arc<HashMap<String, String>>,
    /// Struct field types: maps `struct_name -> field_name -> type_text`.
    /// Enables resolving types of `field_expression` nodes (e.g., `s->count` → "int").
    pub struct_field_types: Arc<HashMap<String, HashMap<String, String>>>,
    /// Names of struct (and typedef-aliased) types declared
    /// `__attribute__((packed))` (directly or via a macro like
    /// `STRUCT_PACKED` whose `#define` expands to packed) across all scanned
    /// files, incl. headers. A packed struct's actual alignment is 1, so
    /// EXP36-C must not treat a cast into it as alignment-increasing.
    #[serde(default)]
    pub packed_structs: Arc<HashSet<String>>,
    /// Names of functions known never to return to their caller, collected
    /// across all scanned files (incl. headers) by
    /// [`crate::analyze::noreturn::collect_noreturn_function_names`]: the
    /// fixed C standard library set, `_Noreturn` qualifiers,
    /// `__attribute__((noreturn))`, and the recovered bare-identifier
    /// attribute macros. Cross-file because the declaration carrying the
    /// attribute is routinely in a header the single-file parse never sees
    /// -- pure-ftpd marks its `no_mem()` allocation-failure helper
    /// `__attribute__((noreturn))` in `ftpd.h` while every call site is in
    /// a `.c` file (task 1076).
    #[serde(default)]
    pub noreturn_functions: Arc<HashSet<String>>,
    /// Global constants: `[const] TYPE NAME = VALUE;` from across all scanned files.
    /// Used by init-state analysis for dead-branch elimination.
    #[serde(default)]
    pub global_constants: HashMap<String, i64>,
    /// Global pointer variable null states from across all scanned files.
    /// Maps variable name to its joined null state across all assignment sites.
    /// Used by EXP34-C to resolve `extern` pointer globals declared in other
    /// translation units (Juliet CWE-476 variant 68 pattern).
    #[serde(default)]
    pub global_var_null_states: Arc<HashMap<String, NullState>>,
    /// File-scope `static` variable writers: maps static-variable name to the
    /// set of function names that assign to it. Used by ENV03-C (and other
    /// taint-aware rules) to decide whether a `char *data = g_static;` read
    /// brings in taint — if every writer's summary is taint-free, the global
    /// is treated as clean. Targets Juliet CWE-78 variant 45 (goodG2BSink
    /// pattern).
    #[serde(default)]
    pub global_writers: Arc<HashMap<String, HashSet<String>>>,
    /// Function-like macro definitions (`#define NAME(a,b) body`) collected
    /// across all scanned files (incl. headers) during the prescan pre-pass.
    /// Consumed by `macro_expand` to expand opaque macro invocations on demand
    /// (Phase 2 of docs/design/macro-expansion.md). Macros using `#`/`##` or
    /// variadics are intentionally excluded (see `macro_expand`).
    #[serde(default)]
    pub function_macros: Arc<HashMap<String, FunctionMacro>>,
    /// Names of every `#define NAME ...` object-like macro collected across
    /// all scanned files (incl. headers), regardless of what they expand to.
    /// Used by DCL40-C to recognize a trailing bare identifier after a
    /// struct/union/enum body (e.g. hostap's `struct foo { ... }
    /// STRUCT_PACKED;`) as an attribute-position macro invocation rather
    /// than a genuine object declaration — the `#define` commonly lives in a
    /// different file than the struct (task 432).
    #[serde(default)]
    pub defined_macro_names: Arc<HashSet<String>>,
    /// Names of every object-like `#define` whose replacement text is an
    /// unused-attribute annotation — `__attribute__((unused))`,
    /// `[[maybe_unused]]`, and the reserved spellings — collected across all
    /// scanned files (incl. headers). seL4's `UNUSED`, hostap's
    /// `STRUCT_PACKED`-adjacent annotations and the rest are recognized by
    /// what they *expand to*, never by name, and the `#define` almost always
    /// lives in a different file from the declaration it annotates.
    ///
    /// Used by MSC13-C: aurora-lint has no preprocessor, so such a macro sits
    /// in the declaration where a type or declarator is expected and the
    /// recovered parse misnames the variable. The annotation is the author
    /// stating the variable may legitimately go unused, which is exactly
    /// what MSC13-C exists to respect, so a declaration carrying one is not
    /// reported at all.
    #[serde(default)]
    pub unused_attribute_macros: Arc<HashSet<String>>,
    /// Functions whose name appears as a bare value inside an aggregate
    /// initializer (e.g. `{ "mysql", pw_mysql_parse, pw_mysql_check,
    /// pw_mysql_exit }` or a designated `.check = pw_mysql_check`) — the
    /// dispatch-table registration idiom used by callback-style backends
    /// (auth/log/protocol handler tables) — and that are never invoked
    /// through a direct-by-name `identifier(...)` call anywhere in the
    /// project. Such a function is reachable only through the single
    /// indirect call site that walks the table, so API00-C treats it like
    /// a project-internal helper (task 594, extending task 169's
    /// internal-contract suppression to the dispatch-table-callback shape).
    #[serde(default)]
    pub dispatch_table_callbacks: HashSet<String>,
    /// `#include` paths that name a *project* header which is not on disk:
    /// the directory prefix resolves under one of the search roots but the
    /// file itself does not exist (e.g. seL4's `<object/structures_gen.h>`,
    /// emitted at build time by `tools/bitfield_gen.py` from an `.bf` spec;
    /// likewise `*.pb-c.h`, `*.tab.h`, and other generated headers).
    ///
    /// A system header that simply isn't on the `-I` path (`<sys/socket.h>`)
    /// does *not* land here — its directory prefix doesn't exist under the
    /// project either — so this set means specifically "this project's
    /// declaration set is incomplete because a build step we can't run
    /// produces part of it" (task 580).
    #[serde(default)]
    pub unresolved_project_headers: HashSet<String>,
    /// Every place the macro-expansion engine declined or failed to see a
    /// definition while building this context — skipped variadic / `#`/`##`
    /// macros, platform-dead and ambiguous definitions, cross-file conflicts,
    /// unresolvable `#include`s. Recorded unconditionally (it is a by-product
    /// of scans that already run) and surfaced only by `--report-macro-gaps`;
    /// nothing in analysis reads it (task 1180).
    #[serde(default)]
    pub macro_gaps: Vec<super::macro_gaps::MacroGap>,
    /// `function name -> indices of its restrict-qualified parameters`, for
    /// every function any scanned file defines or declares with at least one.
    /// First definition seen wins. Lets EXP43-C confine its aliasing check
    /// to callees whose contract actually forbids aliasing (task 1171).
    #[serde(default)]
    pub restrict_params: HashMap<String, Vec<usize>>,
    /// `function name -> indices of the parameters whose doc comment states
    /// a non-NULL precondition` ("must be initialized", "must not be NULL",
    /// ...), from every definition and prototype any scanned file carries a
    /// Doxygen comment for. The function's own published contract, which is
    /// what lets API00-C and the EXP34-C parameter seeding honour a
    /// caller-validates discipline the code documents (task 1171).
    #[serde(default)]
    pub documented_nonnull_params: HashMap<String, Vec<usize>>,
    /// Function names reachable (including the root itself) from a real
    /// concurrent-execution root: an ISR handler, a thread-spawn entry
    /// point (`pthread_create`/`thrd_create`/`CreateThread`, direct or
    /// forwarded through a function-like macro), or a `signal()`-registered
    /// handler. Computed once during prescan by forward-walking
    /// `call_graph` from every detected root (`ambiguous_call_targets`
    /// edges excluded — see that field's docs). Empty when the scanned
    /// project has no such root anywhere (e.g. a genuinely single-threaded
    /// codebase). Used by CON03-C/CON07-C to gate findings on whether the
    /// flagged code is ever reachable from a concurrent context at all,
    /// rather than firing unconditionally (task 608; see
    /// `docs/design/con03-con07-isr-thread-reachability.md`).
    #[serde(default)]
    pub concurrency_reachable: Arc<HashSet<String>>,
    /// Names of project-wide (file-scope, non-local) variables declared with
    /// a plain, non-pointer/non-array/non-function type -- across every
    /// scanned `.c` AND `.h` file, extern declarations included, since the
    /// `extern` forward-declaration and the actual definition are typically
    /// in different files. A name is excluded if it is ever declared with a
    /// pointer or array declarator anywhere in the project (conservative:
    /// only one true global object can exist per name at link time, so
    /// disagreement means something this heuristic shouldn't guess about).
    ///
    /// Mirrors MEM31-C's per-function `value_only_locals`
    /// (`collect_value_only_locals`) but at project scope: seL4's
    /// `current_lookup_fault`/`current_fault` globals are `extern`-declared
    /// in a header and assigned via a bitfield-generator `_new()` value
    /// constructor (`current_lookup_fault = lookup_fault_new(...)`) from
    /// several other translation units, with no local declaration in any of
    /// them -- MEM31-C's per-function pointer-evidence guard can't see a
    /// declaration at all in that shape, so it needs this project-wide set
    /// instead (task 652).
    #[serde(default)]
    pub value_only_globals: Arc<HashSet<String>>,
    /// Struct/union typedef aliases: `alias name -> the tag name its fields
    /// are filed under in `struct_field_types``, for every
    /// `typedef struct Tag Alias;` across the scanned files.
    ///
    /// `collect_from_typedef` files a BODIED typedef under both the tag and
    /// the alias, so the gap this closes is the bodyless spelling:
    /// sqlite's `vdbe.h` says `typedef struct sqlite3_value Mem;` while
    /// `vdbeInt.h` declares `struct sqlite3_value { ... }`, so the fields are
    /// filed under `sqlite3_value` and nothing maps `Mem` onto them. The
    /// typedef and the use are routinely in different files, so no file-local
    /// pass can close it (task 963).
    ///
    /// Deliberately kept OUT of `struct_field_types` itself. That map is read
    /// by INT30-C, INT32-C, INT33-C and FLP03-C, and filing the alias there
    /// would move four other rules' finding sets as a side effect of an
    /// ARR36-C fix; a consumer opts in by resolving through this map, which
    /// so far only ARR36-C does.
    #[serde(default)]
    pub struct_typedef_aliases: Arc<HashMap<String, String>>,
    /// One-level `typedef` alias map: `alias name -> underlying type text as
    /// written` (e.g. `"paddr_t" -> "word_t"`, `"word_t" -> "unsigned long"`),
    /// collected across every scanned `.c`/`.h` file. Simple scalar aliases
    /// only (`typedef <type> <name>;`) -- struct/union/enum-bodied typedefs
    /// are tracked separately by `struct_field_types`, and pointer/array/
    /// function typedefs are excluded since they don't participate in a
    /// scalar signedness chain.
    ///
    /// A typedef's declaring header is frequently not the file that uses the
    /// alias (seL4's `word_t` family: `paddr_t`/`pptr_t`/`vptr_t`/`seL4_Word`
    /// each typedef onto `word_t`, sometimes from an arch-specific header
    /// different from where `word_t` itself is defined), so resolving one
    /// level locally isn't enough -- a consumer must walk this map
    /// recursively (see `overflow_helpers::typedef_chain_is_unsigned`) and
    /// project-wide (task 657).
    #[serde(default)]
    pub typedef_types: Arc<HashMap<String, String>>,
    /// Names of typedefs whose declared type is a function pointer -- e.g.
    /// sqlite's `typedef int (*RecordCompare)(void *, int);` in
    /// `sqliteInt.h`. `collect_from_simple_typedef` filed under
    /// `typedef_types` only stores primitive/sized/named RHSs, so a
    /// function-pointer typedef leaves that map with no entry for its
    /// alias name; DCL31-C needs the *category* (function-pointer
    /// typedef?), not the RHS text, to decide whether a parameter of
    /// that type is directly callable (task 1054, second consumer of
    /// task 736's shared typedef-chain resolver).
    #[serde(default)]
    pub function_pointer_typedef_names: Arc<HashSet<String>>,
    /// Names of typedefs that hide a pointer in DCL05-C's sense -- a pointer
    /// in the declarator chain, not a function pointer, not a pointer to
    /// const (`declarator_utils::pointer_typedef_names_in`). The typedef is
    /// usually in a header; the `const LPPOINT pt` parameter that the rule
    /// is about is in a .c file that only names the alias (task 1188).
    #[serde(default)]
    pub pointer_typedef_names: Arc<HashSet<String>>,
}

impl ProjectContext {
    /// An empty context, as if nothing had been pre-scanned yet.
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns `true` if the given name was found during the pre-scan.
    pub fn is_known_function(&self, name: &str) -> bool {
        self.known_functions.contains(name)
    }

    /// Returns the summary for a function, if available.
    pub fn get_function_summary(&self, name: &str) -> Option<&FunctionSummary> {
        self.function_summaries.get(name)
    }

    /// Returns `true` if the function has a prototype in a `.h` header file,
    /// indicating it is public API with intentional external linkage.
    pub fn is_header_declared(&self, name: &str) -> bool {
        self.header_declared_functions.contains(name)
    }

    /// Look up the type of a struct field given the struct name and field name.
    /// `struct_name` should be the bare name (e.g., "MyStruct", not "struct MyStruct").
    pub fn get_struct_field_type(&self, struct_name: &str, field_name: &str) -> Option<&str> {
        self.struct_field_types
            .get(struct_name)
            .and_then(|fields| fields.get(field_name))
            .map(|s| s.as_str())
    }

    /// Returns `true` if any cross-file data was collected.
    ///
    /// `header_declared_functions` is included so that a lightweight
    /// header-only prescan (no `-d` flag) still triggers `set_project_context`
    /// on rules like DCL15-C that only need the public-API declaration set.
    pub fn has_cross_file_data(&self) -> bool {
        !self.known_functions.is_empty()
            || !self.function_summaries.is_empty()
            || !self.macro_constants.is_empty()
            || !self.struct_field_types.is_empty()
            || !self.header_declared_functions.is_empty()
            || !self.typedef_types.is_empty()
    }

    /// Save prescan context to a binary cache file.
    pub fn save_to_file(&self, path: &Path) -> anyhow::Result<()> {
        let encoded = bincode::serialize(self)?;
        std::fs::write(path, &encoded)?;
        Ok(())
    }

    /// Load prescan context from a binary cache file.
    pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
        let data = std::fs::read(path)?;
        let context: Self = bincode::deserialize(&data)?;
        Ok(context)
    }
}