mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
1328 lines
43 KiB
Rust
1328 lines
43 KiB
Rust
//! Native port of the cFnPtr synthesizer's EXTRACTION SWEEP (task #5 step 2,
|
||
//! plan §7a.9): raw file text in → collected per-file facts out. The linking
|
||
//! stages, gates, and every registration/dispatch decision stay TS-side; this
|
||
//! module only reproduces, bug-for-bug, what the JS sweep computes per file in
|
||
//! `src/resolution/c-fnptr-synthesizer.ts`:
|
||
//!
|
||
//! • `stripCommentsForRegex(text, 'c')` — the C-style comment/string state
|
||
//! machine (comments blanked to spaces, string interiors skipped, backtick
|
||
//! treated as a multi-line string delimiter — quirks and all);
|
||
//! • the typedef scans (fn-pointer + fn-type forms);
|
||
//! • struct-node field declarations (structural parse; classification stays
|
||
//! TS-side where the complete typedef sets live);
|
||
//! • the survival-filter scans (inline structs, initializers, bare arrays,
|
||
//! alias-shaped object macros, field-assign pairs, dispatch fields, array
|
||
//! dispatch names);
|
||
//! • the raw-text `#include "..."` capture (path resolution stays TS-side —
|
||
//! it needs the filesystem).
|
||
//!
|
||
//! Parity discipline: the JS side runs these as JavaScript REGEXES, so every
|
||
//! scanner here is a hand-rolled byte machine replicating THAT engine's
|
||
//! semantics, not idiomatic Rust regex:
|
||
//! • JS `\w`/`\b` are ASCII (non-ASCII chars are non-word) — byte checks
|
||
//! against `[A-Za-z0-9_]` reproduce them exactly, because UTF-8
|
||
//! continuation bytes are non-ASCII and therefore non-word on both sides.
|
||
//! • JS `\s` is the UNICODE whitespace class (NBSP, U+2000-200A, U+FEFF, …)
|
||
//! — `jsws_len` decodes exactly that set from UTF-8.
|
||
//! • Backtracking is reproduced where it is observable (INIT/ARRAY modifier
|
||
//! and `struct` keyword ambiguity, DISPATCH's greedy segment loop,
|
||
//! optional groups) and elided only where analysis shows no input can
|
||
//! distinguish greedy from backtracked (documented per scanner).
|
||
//! • `lastIndex` advancement (resume after each match, +1 on failure) is
|
||
//! reproduced so overlapping-match selection is identical.
|
||
//!
|
||
//! The stripper blanks per UTF-16 code unit (see `strip_c`), so its output
|
||
//! equals the JS stripper's output EXACTLY as a string — every scanner here
|
||
//! runs over the identical character stream the JS regexes see, and the strip
|
||
//! differential oracle test pins that equality directly. The record-level
|
||
//! differential suite (JS sweep vs this sweep over fixtures and whole repos)
|
||
//! then pins the scanners themselves.
|
||
|
||
/// One struct node's extent, as the TS side reads it from the graph.
|
||
pub struct StructExtent {
|
||
pub id: String,
|
||
pub start_line: u32,
|
||
pub end_line: u32,
|
||
}
|
||
|
||
/// A structurally-parsed struct field — mirror of the TS `RawFieldDecl`
|
||
/// (`name: null` is represented as an empty string; the TS side treats them
|
||
/// identically everywhere).
|
||
pub struct RawField {
|
||
pub name: String,
|
||
pub index: u32,
|
||
pub ptr: bool,
|
||
pub ty: String,
|
||
}
|
||
|
||
pub struct StructFields {
|
||
pub id: String,
|
||
/// False when the body never parsed (no `{`, unbalanced braces, or a
|
||
/// falsy start line) — the TS side then records nothing for this node,
|
||
/// exactly like the JS sweep.
|
||
pub parsed: bool,
|
||
pub fields: Vec<RawField>,
|
||
}
|
||
|
||
/// Everything the sweep collects for one file.
|
||
pub struct FileFacts {
|
||
pub fn_ptr_typedefs: Vec<String>,
|
||
pub fn_type_typedefs: Vec<String>,
|
||
pub structs: Vec<StructFields>,
|
||
pub inline_ptr: bool,
|
||
pub inline_types: Vec<String>,
|
||
pub inline_tags: Vec<String>,
|
||
pub init_tokens: Vec<String>,
|
||
/// `*`-prefixed when the declaration carried the pointer star.
|
||
pub array_elems: Vec<String>,
|
||
pub alias_names: Vec<String>,
|
||
/// `lfield\0rfield`, distinct.
|
||
pub d_pairs: Vec<String>,
|
||
pub dispatch_fields: Vec<String>,
|
||
pub array_dispatch_names: Vec<String>,
|
||
/// Raw `#include "…"` captures, in source order, NOT deduplicated —
|
||
/// extension filtering and path resolution happen TS-side.
|
||
pub includes: Vec<String>,
|
||
}
|
||
|
||
/// Mirror of the TS `C_TYPE_KEYWORDS` set — keep in exact sync.
|
||
const C_TYPE_KEYWORDS: [&[u8]; 17] = [
|
||
b"void", b"int", b"char", b"short", b"long", b"unsigned", b"signed", b"float", b"double",
|
||
b"const", b"struct", b"union", b"enum", b"static", b"volatile", b"register", b"inline",
|
||
];
|
||
|
||
fn is_type_keyword(w: &[u8]) -> bool {
|
||
C_TYPE_KEYWORDS.iter().any(|k| *k == w)
|
||
}
|
||
|
||
const MODIFIERS: [&[u8]; 5] = [b"static", b"const", b"extern", b"register", b"volatile"];
|
||
|
||
#[inline]
|
||
fn is_word(b: u8) -> bool {
|
||
b.is_ascii_alphanumeric() || b == b'_'
|
||
}
|
||
|
||
#[inline]
|
||
fn is_word_at(s: &[u8], i: usize) -> bool {
|
||
i < s.len() && is_word(s[i])
|
||
}
|
||
|
||
/// Byte length of the JS `\s` character starting at `i`, or 0 when `s[i]`
|
||
/// doesn't start one. JS \s = [\t\n\v\f\r -
|
||
///
].
|
||
#[inline]
|
||
fn jsws_len(s: &[u8], i: usize) -> usize {
|
||
let Some(&b0) = s.get(i) else { return 0 };
|
||
match b0 {
|
||
0x09..=0x0D | 0x20 => 1,
|
||
0xC2 if s.get(i + 1) == Some(&0xA0) => 2, // U+00A0
|
||
0xE1 if s.get(i + 1) == Some(&0x9A) && s.get(i + 2) == Some(&0x80) => 3, // U+1680
|
||
0xE2 => match (s.get(i + 1), s.get(i + 2)) {
|
||
(Some(&0x80), Some(&b2)) if (0x80..=0x8A).contains(&b2) => 3, // U+2000-200A
|
||
(Some(&0x80), Some(&0xA8)) => 3, // U+2028
|
||
(Some(&0x80), Some(&0xA9)) => 3, // U+2029
|
||
(Some(&0x80), Some(&0xAF)) => 3, // U+202F
|
||
(Some(&0x81), Some(&0x9F)) => 3, // U+205F
|
||
_ => 0,
|
||
},
|
||
0xE3 if s.get(i + 1) == Some(&0x80) && s.get(i + 2) == Some(&0x80) => 3, // U+3000
|
||
0xEF if s.get(i + 1) == Some(&0xBB) && s.get(i + 2) == Some(&0xBF) => 3, // U+FEFF
|
||
_ => 0,
|
||
}
|
||
}
|
||
|
||
/// Advance past `\s*`.
|
||
#[inline]
|
||
fn skip_jsws(s: &[u8], mut i: usize) -> usize {
|
||
loop {
|
||
let l = jsws_len(s, i);
|
||
if l == 0 {
|
||
return i;
|
||
}
|
||
i += l;
|
||
}
|
||
}
|
||
|
||
/// End of the `\w+` run starting at `i` (caller checks `is_word_at(s, i)`).
|
||
#[inline]
|
||
fn word_end(s: &[u8], mut i: usize) -> usize {
|
||
while i < s.len() && is_word(s[i]) {
|
||
i += 1;
|
||
}
|
||
i
|
||
}
|
||
|
||
/// JS `\b` before position `i` (position 0, or previous byte non-word).
|
||
#[inline]
|
||
fn boundary_before(s: &[u8], i: usize) -> bool {
|
||
i == 0 || !is_word(s[i - 1])
|
||
}
|
||
|
||
fn find_bytes(s: &[u8], needle: &[u8], from: usize) -> Option<usize> {
|
||
if needle.is_empty() || s.len() < needle.len() {
|
||
return None;
|
||
}
|
||
let mut i = from;
|
||
while i + needle.len() <= s.len() {
|
||
// memchr on the first byte keeps this fast on 20KB+ files.
|
||
match s[i..s.len() - needle.len() + 1].iter().position(|&b| b == needle[0]) {
|
||
None => return None,
|
||
Some(off) => {
|
||
i += off;
|
||
if &s[i..i + needle.len()] == needle {
|
||
return Some(i);
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn contains_bytes(s: &[u8], needle: &[u8]) -> bool {
|
||
find_bytes(s, needle, 0).is_some()
|
||
}
|
||
|
||
/// `\bWORD\b` occurrence search from `from`.
|
||
fn find_word(s: &[u8], word: &[u8], mut from: usize) -> Option<usize> {
|
||
loop {
|
||
let t = find_bytes(s, word, from)?;
|
||
if boundary_before(s, t) && !is_word_at(s, t + word.len()) {
|
||
return Some(t);
|
||
}
|
||
from = t + 1;
|
||
}
|
||
}
|
||
|
||
// ---------- stripCommentsForRegex(src, 'c') ----------
|
||
|
||
/// Port of `stripCStyle(src, /*allowSingleQuoteStrings*/ false)`:
|
||
/// `/* */` and `//` comments blanked to spaces (newlines preserved), `"` and
|
||
/// backtick string interiors skipped verbatim (backtick spans lines — the JS
|
||
/// helper treats it as a template literal even for C), `'` NOT special.
|
||
///
|
||
/// Blanking is per UTF-16 CODE UNIT (one space per BMP char, two per astral
|
||
/// char), so the result equals the JS stripper's output EXACTLY as a string —
|
||
/// the scanners downstream see the identical character stream the JS regexes
|
||
/// see, and the strip differential oracle pins byte equality directly.
|
||
/// (Comment boundaries — `/*`, `*/`, `//`, quotes, `\n` — are all ASCII, so
|
||
/// the state machine's byte positions always land on char boundaries.)
|
||
pub fn strip_c(src: &[u8]) -> Vec<u8> {
|
||
let n = src.len();
|
||
let mut out = Vec::with_capacity(n);
|
||
let mut copied = 0usize; // src[..copied] already emitted
|
||
let mut i = 0;
|
||
{
|
||
let mut blank_to = |out: &mut Vec<u8>, start: usize, end: usize| {
|
||
out.extend_from_slice(&src[copied..start]);
|
||
emit_blank(out, &src[start..end]);
|
||
copied = end;
|
||
};
|
||
while i < n {
|
||
let c = src[i];
|
||
let c2 = if i + 1 < n { src[i + 1] } else { 0 };
|
||
if c == b'/' && c2 == b'*' {
|
||
let start = i;
|
||
i += 2;
|
||
while i < n && !(src[i] == b'*' && i + 1 < n && src[i + 1] == b'/') {
|
||
i += 1;
|
||
}
|
||
if i < n {
|
||
i += 2;
|
||
}
|
||
blank_to(&mut out, start, i.min(n));
|
||
continue;
|
||
}
|
||
if c == b'/' && c2 == b'/' {
|
||
let start = i;
|
||
while i < n && src[i] != b'\n' {
|
||
i += 1;
|
||
}
|
||
blank_to(&mut out, start, i);
|
||
continue;
|
||
}
|
||
if c == b'"' || c == b'`' {
|
||
let quote = c;
|
||
i += 1;
|
||
while i < n && src[i] != quote {
|
||
if src[i] == b'\\' && i + 1 < n {
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if quote != b'`' && src[i] == b'\n' {
|
||
break;
|
||
}
|
||
i += 1;
|
||
}
|
||
if i < n && src[i] == quote {
|
||
i += 1;
|
||
}
|
||
continue;
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
out.extend_from_slice(&src[copied..]);
|
||
out
|
||
}
|
||
|
||
/// One space per UTF-16 code unit (`\n` preserved): ASCII and 2-3-byte chars
|
||
/// are one unit, 4-byte (astral) chars are a surrogate pair — two units.
|
||
fn emit_blank(out: &mut Vec<u8>, region: &[u8]) {
|
||
let mut i = 0;
|
||
while i < region.len() {
|
||
let b = region[i];
|
||
if b == b'\n' {
|
||
out.push(b'\n');
|
||
i += 1;
|
||
continue;
|
||
}
|
||
let len = if b < 0x80 {
|
||
1
|
||
} else if b < 0xC0 {
|
||
1 // continuation byte at region start — invalid UTF-8; count singly
|
||
} else if b < 0xE0 {
|
||
2
|
||
} else if b < 0xF0 {
|
||
3
|
||
} else {
|
||
4
|
||
};
|
||
out.push(b' ');
|
||
if len == 4 {
|
||
out.push(b' ');
|
||
}
|
||
i += len.min(region.len() - i);
|
||
}
|
||
}
|
||
|
||
// ---------- shared regex tails ----------
|
||
|
||
/// `\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(` matched at `open` (which must hold
|
||
/// `(`). Returns (name_range, end_after_second_paren). The `(?:\w+\s+)*`
|
||
/// group is greedy without backtracking: giving back an iteration repositions
|
||
/// `\*` onto a word char, which can never match, so greedy ≡ backtracked.
|
||
fn fnptr_paren_tail(s: &[u8], open: usize) -> Option<((usize, usize), usize)> {
|
||
let mut i = skip_jsws(s, open + 1);
|
||
loop {
|
||
if !is_word_at(s, i) {
|
||
break;
|
||
}
|
||
let we = word_end(s, i);
|
||
let wse = skip_jsws(s, we);
|
||
if wse == we {
|
||
break; // \w+ not followed by \s+ — the iteration fails, word not consumed
|
||
}
|
||
i = wse;
|
||
}
|
||
if s.get(i) != Some(&b'*') {
|
||
return None;
|
||
}
|
||
i = skip_jsws(s, i + 1);
|
||
if !is_word_at(s, i) {
|
||
return None;
|
||
}
|
||
let name = (i, word_end(s, i));
|
||
i = skip_jsws(s, name.1);
|
||
if s.get(i) != Some(&b')') {
|
||
return None;
|
||
}
|
||
i = skip_jsws(s, i + 1);
|
||
if s.get(i) != Some(&b'(') {
|
||
return None;
|
||
}
|
||
Some((name, i + 1))
|
||
}
|
||
|
||
/// `\s*\)?\s*\(` at `i` → position after the `(`. The optional `)` needs no
|
||
/// backtracking: retrying without a consumed `)` lands `\(` on that `)`.
|
||
fn close_call_tail(s: &[u8], i: usize) -> Option<usize> {
|
||
let mut j = skip_jsws(s, i);
|
||
if s.get(j) == Some(&b')') {
|
||
j = skip_jsws(s, j + 1);
|
||
}
|
||
if s.get(j) == Some(&b'(') {
|
||
return Some(j + 1);
|
||
}
|
||
None
|
||
}
|
||
|
||
// ---------- scanners ----------
|
||
|
||
/// FNPTR_TYPEDEF_RE: /\btypedef\b[^;{}]*?\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/g
|
||
fn scan_fnptr_typedefs(s: &[u8], out: &mut Vec<String>) {
|
||
let mut last = 0;
|
||
while let Some(t) = find_word(s, b"typedef", last) {
|
||
let mut j = t + 7;
|
||
let mut matched = None;
|
||
// Lazy [^;{}]*?: try the paren tail at each `(` in order; the class
|
||
// may also expand ACROSS a failed `(` (it admits parens).
|
||
while j < s.len() {
|
||
let ch = s[j];
|
||
if ch == b';' || ch == b'{' || ch == b'}' {
|
||
break;
|
||
}
|
||
if ch == b'(' {
|
||
if let Some((name, end)) = fnptr_paren_tail(s, j) {
|
||
matched = Some((name, end));
|
||
break;
|
||
}
|
||
}
|
||
j += 1;
|
||
}
|
||
match matched {
|
||
Some(((ns, ne), end)) => {
|
||
push_str(out, &s[ns..ne]);
|
||
last = end;
|
||
}
|
||
None => last = t + 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// FNTYPE_TYPEDEF_STMT_RE (/\btypedef\b([^;{}]*);/g) + the TS-side guts
|
||
/// checks: skip when guts contains `(*` or `( *`; else the FIRST
|
||
/// /\b(\w+)\s*\(/ capture, filtered through C_TYPE_KEYWORDS.
|
||
fn scan_fntype_typedefs(s: &[u8], out: &mut Vec<String>) {
|
||
let mut last = 0;
|
||
while let Some(t) = find_word(s, b"typedef", last) {
|
||
let mut j = t + 7;
|
||
while j < s.len() && s[j] != b';' && s[j] != b'{' && s[j] != b'}' {
|
||
j += 1;
|
||
}
|
||
if j >= s.len() || s[j] != b';' {
|
||
last = t + 1;
|
||
continue;
|
||
}
|
||
let guts = &s[t + 7..j];
|
||
if !contains_bytes(guts, b"(*") && !contains_bytes(guts, b"( *") {
|
||
// first \b(\w+)\s*\( in guts
|
||
let mut p = 0;
|
||
while p < guts.len() {
|
||
if is_word(guts[p]) && boundary_before(guts, p) {
|
||
let we = word_end(guts, p);
|
||
let k = skip_jsws(guts, we);
|
||
if guts.get(k) == Some(&b'(') {
|
||
let w = &guts[p..we];
|
||
if !is_type_keyword(w) {
|
||
push_str(out, w);
|
||
}
|
||
break;
|
||
}
|
||
p = we;
|
||
} else {
|
||
p += 1;
|
||
}
|
||
}
|
||
}
|
||
last = j + 1;
|
||
}
|
||
}
|
||
|
||
/// INLINE_STRUCT_RE (/\bstruct\s+(\w+)\s*\{/g), sweep flavor: NO cursor jump
|
||
/// (the filter needs a superset of the registration pass's jump-scan), each
|
||
/// valid candidate (balanced braces + the `^\s*(\w+)…` var check) contributes
|
||
/// its tag and a structural field summary.
|
||
struct InlineScan {
|
||
ptr: bool,
|
||
types: Vec<String>,
|
||
tags: Vec<String>,
|
||
}
|
||
|
||
fn scan_inline_structs(s: &[u8]) -> InlineScan {
|
||
let mut out = InlineScan { ptr: false, types: Vec::new(), tags: Vec::new() };
|
||
let mut last = 0;
|
||
loop {
|
||
let next_struct = find_word(s, b"struct", last);
|
||
let next_union = find_word(s, b"union", last);
|
||
let Some((t, keyword_len)) = (match (next_struct, next_union) {
|
||
(Some(st), Some(un)) if st < un => Some((st, 6)),
|
||
(Some(_), Some(un)) => Some((un, 5)),
|
||
(Some(st), None) => Some((st, 6)),
|
||
(None, Some(un)) => Some((un, 5)),
|
||
(None, None) => None,
|
||
}) else {
|
||
break;
|
||
};
|
||
let after_kw = t + keyword_len;
|
||
let ws = skip_jsws(s, after_kw);
|
||
if ws == after_kw || !is_word_at(s, ws) {
|
||
last = t + 1;
|
||
continue;
|
||
}
|
||
let te = word_end(s, ws);
|
||
let open = skip_jsws(s, te);
|
||
if s.get(open) != Some(&b'{') {
|
||
last = t + 1;
|
||
continue;
|
||
}
|
||
last = open + 1; // lastIndex = end of match (after `{`)
|
||
let Some(close) = match_brace(s, open) else { continue };
|
||
// vm: /^\s*(\w+)…/ on the text after `}` — only vm[1] matters here.
|
||
let v = skip_jsws(s, close + 1);
|
||
if !is_word_at(s, v) {
|
||
continue;
|
||
}
|
||
push_str(&mut out.tags, &s[ws..te]);
|
||
for f in parse_struct_fields_raw(&s[open + 1..close]) {
|
||
if f.name.is_empty() {
|
||
continue;
|
||
}
|
||
if f.ptr {
|
||
out.ptr = true;
|
||
} else if !f.ty.is_empty() {
|
||
out.types.push(f.ty);
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// matchBrace: index of the `}` matching the `{` at `open`, or None.
|
||
fn match_brace(s: &[u8], open: usize) -> Option<usize> {
|
||
let mut depth = 0i64;
|
||
let mut i = open;
|
||
while i < s.len() {
|
||
match s[i] {
|
||
b'{' => depth += 1,
|
||
b'}' => {
|
||
depth -= 1;
|
||
if depth == 0 {
|
||
return Some(i);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
i += 1;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The `(?:(?:static|const|extern|register|volatile)\s+)*` modifier loop:
|
||
/// greedy positions after 0..=k iterations, for the k-descending backtrack the
|
||
/// INIT/ARRAY skeletons need. No two alternatives share a prefix, so at most
|
||
/// one literal can match at a position; an alternative that matches without
|
||
/// trailing `\s+` ends the loop (JS: iteration fails, no other alt can fire).
|
||
fn modifier_positions(s: &[u8], start: usize) -> Vec<usize> {
|
||
let mut stack = vec![start];
|
||
loop {
|
||
let cur = *stack.last().unwrap();
|
||
let mut advanced = None;
|
||
for m in MODIFIERS {
|
||
if s.len() >= cur + m.len() && &s[cur..cur + m.len()] == m {
|
||
let e = cur + m.len();
|
||
let w = skip_jsws(s, e);
|
||
if w > e {
|
||
advanced = Some(w);
|
||
}
|
||
break; // exactly one alternative can literal-match here
|
||
}
|
||
}
|
||
match advanced {
|
||
Some(w) => stack.push(w),
|
||
None => return stack,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `\[[^\]]*\]` at `i` (the INIT/ARRAY declarator form — the class admits
|
||
/// newlines): position after the FIRST `]`, or None.
|
||
fn bracket_span(s: &[u8], i: usize) -> Option<usize> {
|
||
if s.get(i) != Some(&b'[') {
|
||
return None;
|
||
}
|
||
let mut j = i + 1;
|
||
while j < s.len() && s[j] != b']' {
|
||
j += 1;
|
||
}
|
||
if j < s.len() {
|
||
Some(j + 1)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Anchor-skeleton driver shared by INIT_RE and ARRAY_TABLE_RE: both match
|
||
/// `(?:^|[;{}])` then a body, and resume from the end of each match. `body`
|
||
/// returns (token, match_end) when the body matches at the position after the
|
||
/// anchor.
|
||
fn scan_anchored<F>(s: &[u8], mut body: F, out: &mut Vec<String>)
|
||
where
|
||
F: FnMut(&[u8], usize) -> Option<(String, usize)>,
|
||
{
|
||
let mut last = 0usize;
|
||
// The `^` branch consumes nothing and only exists at position 0.
|
||
if last == 0 {
|
||
if let Some((tok, end)) = body(s, 0) {
|
||
out.push(tok);
|
||
last = end;
|
||
}
|
||
}
|
||
let mut p = last;
|
||
while p < s.len() {
|
||
let ch = s[p];
|
||
if ch == b';' || ch == b'{' || ch == b'}' {
|
||
if let Some((tok, end)) = body(s, p + 1) {
|
||
out.push(tok);
|
||
p = end;
|
||
continue;
|
||
}
|
||
}
|
||
p += 1;
|
||
}
|
||
}
|
||
|
||
/// INIT_RE body after the anchor:
|
||
/// `\s*(?:MOD\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{`
|
||
/// Backtracks: modifier count (desc), `struct` with/without, bracket
|
||
/// with/without — exactly the observable dimensions of the JS engine.
|
||
fn init_body(s: &[u8], p: usize) -> Option<(String, usize)> {
|
||
let i = skip_jsws(s, p);
|
||
let mods = modifier_positions(s, i);
|
||
for &pos in mods.iter().rev() {
|
||
for keyword in [Some(b"struct".as_slice()), Some(b"union".as_slice()), None] {
|
||
let q = if let Some(keyword) = keyword {
|
||
if s.len() >= pos + keyword.len() && &s[pos..pos + keyword.len()] == keyword {
|
||
let e = pos + keyword.len();
|
||
let w = skip_jsws(s, e);
|
||
if w == e {
|
||
continue;
|
||
}
|
||
w
|
||
} else {
|
||
continue;
|
||
}
|
||
} else {
|
||
pos
|
||
};
|
||
if !is_word_at(s, q) {
|
||
continue;
|
||
}
|
||
let te = word_end(s, q);
|
||
let w = skip_jsws(s, te);
|
||
if w == te {
|
||
continue; // \s+ needs ≥1
|
||
}
|
||
if !is_word_at(s, w) {
|
||
continue;
|
||
}
|
||
let ne = word_end(s, w);
|
||
let r = skip_jsws(s, ne);
|
||
for with_bracket in [true, false] {
|
||
let r2 = if with_bracket {
|
||
match bracket_span(s, r) {
|
||
Some(e) => e,
|
||
None => continue,
|
||
}
|
||
} else {
|
||
r
|
||
};
|
||
let r3 = skip_jsws(s, r2);
|
||
if s.get(r3) != Some(&b'=') {
|
||
continue;
|
||
}
|
||
let r4 = skip_jsws(s, r3 + 1);
|
||
if s.get(r4) != Some(&b'{') {
|
||
continue;
|
||
}
|
||
return Some((bytes_to_string(&s[q..te]), r4 + 1));
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// ARRAY_TABLE_RE body after the anchor:
|
||
/// `\s*(?:MOD\s+)*(\w+)\s+(\*\s*)?(\w+)\s*\[[^\]]*\]\s*=\s*\{`
|
||
/// Token is `*`-prefixed when the star declarator is present.
|
||
fn array_table_body(s: &[u8], p: usize) -> Option<(String, usize)> {
|
||
let i = skip_jsws(s, p);
|
||
let mods = modifier_positions(s, i);
|
||
for &pos in mods.iter().rev() {
|
||
if !is_word_at(s, pos) {
|
||
continue;
|
||
}
|
||
let te = word_end(s, pos);
|
||
let w = skip_jsws(s, te);
|
||
if w == te {
|
||
continue;
|
||
}
|
||
for with_star in [true, false] {
|
||
let q = if with_star {
|
||
if s.get(w) == Some(&b'*') {
|
||
skip_jsws(s, w + 1)
|
||
} else {
|
||
continue;
|
||
}
|
||
} else {
|
||
w
|
||
};
|
||
if !is_word_at(s, q) {
|
||
continue;
|
||
}
|
||
let ne = word_end(s, q);
|
||
let r = skip_jsws(s, ne);
|
||
let Some(r2) = bracket_span(s, r) else { continue };
|
||
let r3 = skip_jsws(s, r2);
|
||
if s.get(r3) != Some(&b'=') {
|
||
continue;
|
||
}
|
||
let r4 = skip_jsws(s, r3 + 1);
|
||
if s.get(r4) != Some(&b'{') {
|
||
continue;
|
||
}
|
||
let mut tok = String::new();
|
||
if with_star {
|
||
tok.push('*');
|
||
}
|
||
tok.push_str(&bytes_to_string(&s[pos..te]));
|
||
return Some((tok, r4 + 1));
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// OBJ_ALIAS_RE over the continuation-joined text:
|
||
/// /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:struct[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm
|
||
fn scan_alias_names(stripped: &[u8], out: &mut Vec<String>) {
|
||
// joined = stripped.replace(/\\\r?\n/g, ' ')
|
||
let mut joined = Vec::with_capacity(stripped.len());
|
||
let mut i = 0;
|
||
while i < stripped.len() {
|
||
let b = stripped[i];
|
||
if b == b'\\' {
|
||
if stripped.get(i + 1) == Some(&b'\n') {
|
||
joined.push(b' ');
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if stripped.get(i + 1) == Some(&b'\r') && stripped.get(i + 2) == Some(&b'\n') {
|
||
joined.push(b' ');
|
||
i += 3;
|
||
continue;
|
||
}
|
||
}
|
||
joined.push(b);
|
||
i += 1;
|
||
}
|
||
for line in joined.split(|&b| b == b'\n') {
|
||
if let Some(name) = alias_line(line) {
|
||
push_str(out, name);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn skip_sp_tab(line: &[u8], mut i: usize) -> usize {
|
||
while i < line.len() && (line[i] == b' ' || line[i] == b'\t') {
|
||
i += 1;
|
||
}
|
||
i
|
||
}
|
||
|
||
fn alias_line(line: &[u8]) -> Option<&[u8]> {
|
||
let mut i = skip_sp_tab(line, 0);
|
||
if line.get(i) != Some(&b'#') {
|
||
return None;
|
||
}
|
||
i = skip_sp_tab(line, i + 1);
|
||
if line.len() < i + 6 || &line[i..i + 6] != b"define" {
|
||
return None;
|
||
}
|
||
i += 6;
|
||
let w = skip_sp_tab(line, i);
|
||
if w == i || !is_word_at(line, w) {
|
||
return None;
|
||
}
|
||
let name_end = word_end(line, w);
|
||
let name = &line[w..name_end];
|
||
let v0 = skip_sp_tab(line, name_end);
|
||
if v0 == name_end {
|
||
return None; // [ \t]+ before the value
|
||
}
|
||
// (?:(?:struct|union)[ \t]+)* greedy, k-descending on value failure.
|
||
let mut stack = vec![v0];
|
||
loop {
|
||
let cur = *stack.last().unwrap();
|
||
let keyword_len = if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
|
||
Some(6)
|
||
} else if line.len() >= cur + 5 && &line[cur..cur + 5] == b"union" {
|
||
Some(5)
|
||
} else {
|
||
None
|
||
};
|
||
if let Some(keyword_len) = keyword_len {
|
||
let e = cur + keyword_len;
|
||
let w2 = skip_sp_tab(line, e);
|
||
if w2 > e {
|
||
stack.push(w2);
|
||
continue;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
for &vp in stack.iter().rev() {
|
||
let Some(&b0) = line.get(vp) else { continue };
|
||
if !(b0.is_ascii_alphabetic() || b0 == b'_') {
|
||
continue; // value must start [A-Za-z_]
|
||
}
|
||
let ve = word_end(line, vp);
|
||
// [ \t\r]*$
|
||
let mut t = ve;
|
||
while t < line.len() && (line[t] == b' ' || line[t] == b'\t' || line[t] == b'\r') {
|
||
t += 1;
|
||
}
|
||
if t == line.len() {
|
||
return Some(name);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// FIELD_ASSIGN_RE: /(\w+)\s*(?:->|\.)\s*(\w+)\s*=\s*(\w+)\s*(?:->|\.)\s*(\w+)/g
|
||
/// Pairs collected as `lfield\0rfield`. Every byte position is a candidate
|
||
/// start (JS advances one unit on failure — suffix starts included); matches
|
||
/// resume at their end.
|
||
fn scan_field_assign(s: &[u8], out: &mut Vec<String>) {
|
||
let mut pos = 0usize;
|
||
while pos < s.len() {
|
||
if !is_word(s[pos]) {
|
||
pos += 1;
|
||
continue;
|
||
}
|
||
match field_assign_at(s, pos) {
|
||
Some((lf, rf, end)) => {
|
||
let mut pair = bytes_to_string(&s[lf.0..lf.1]);
|
||
pair.push('\0');
|
||
pair.push_str(&bytes_to_string(&s[rf.0..rf.1]));
|
||
out.push(pair);
|
||
pos = end;
|
||
}
|
||
None => pos += 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn arrow_at(s: &[u8], i: usize) -> Option<usize> {
|
||
if s.get(i) == Some(&b'-') && s.get(i + 1) == Some(&b'>') {
|
||
Some(i + 2)
|
||
} else if s.get(i) == Some(&b'.') {
|
||
Some(i + 1)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
type Range = (usize, usize);
|
||
|
||
fn field_assign_at(s: &[u8], p: usize) -> Option<(Range, Range, usize)> {
|
||
let w1 = word_end(s, p);
|
||
let a1 = arrow_at(s, skip_jsws(s, w1))?;
|
||
let f1s = skip_jsws(s, a1);
|
||
if !is_word_at(s, f1s) {
|
||
return None;
|
||
}
|
||
let f1e = word_end(s, f1s);
|
||
let eq = skip_jsws(s, f1e);
|
||
if s.get(eq) != Some(&b'=') {
|
||
return None;
|
||
}
|
||
let r1s = skip_jsws(s, eq + 1);
|
||
if !is_word_at(s, r1s) {
|
||
return None;
|
||
}
|
||
let r1e = word_end(s, r1s);
|
||
let a2 = arrow_at(s, skip_jsws(s, r1e))?;
|
||
let f2s = skip_jsws(s, a2);
|
||
if !is_word_at(s, f2s) {
|
||
return None;
|
||
}
|
||
let f2e = word_end(s, f2s);
|
||
Some(((f1s, f1e), (f2s, f2e), f2e))
|
||
}
|
||
|
||
/// DISPATCH_RE: /((?:\w+(?:\s*\[[^\][]*\])?\s*(?:->|\.)\s*)+)(\w+)\s*\)?\s*\(/g
|
||
/// The `+` loop is consumed greedily, then the field tail is tried at each
|
||
/// segment count k descending — the JS engine's observable backtracking. The
|
||
/// per-segment optional subscript needs no cross-product: the with/without
|
||
/// parses diverge at the arrow and at most one can complete a segment.
|
||
fn scan_dispatch(s: &[u8], out: &mut Vec<String>) {
|
||
let mut pos = 0usize;
|
||
while pos < s.len() {
|
||
if !is_word(s[pos]) {
|
||
pos += 1;
|
||
continue;
|
||
}
|
||
// Greedy segment loop.
|
||
let mut seg_ends: Vec<usize> = Vec::new();
|
||
let mut cur = pos;
|
||
while is_word_at(s, cur) {
|
||
let we = word_end(s, cur);
|
||
let with_sub = subscript_span(s, skip_jsws(s, we)).and_then(|e| arrow_tail(s, e));
|
||
let seg = with_sub.or_else(|| arrow_tail(s, we));
|
||
match seg {
|
||
Some(e) => {
|
||
seg_ends.push(e);
|
||
cur = e;
|
||
}
|
||
None => break,
|
||
}
|
||
}
|
||
let mut matched = None;
|
||
for k in (1..=seg_ends.len()).rev() {
|
||
let fpos = seg_ends[k - 1];
|
||
if !is_word_at(s, fpos) {
|
||
continue;
|
||
}
|
||
let fe = word_end(s, fpos);
|
||
if let Some(end) = close_call_tail(s, fe) {
|
||
matched = Some(((fpos, fe), end));
|
||
break;
|
||
}
|
||
}
|
||
match matched {
|
||
Some(((fs_, fe), end)) => {
|
||
push_str(out, &s[fs_..fe]);
|
||
pos = end;
|
||
}
|
||
None => pos += 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `\[[^\][]*\]` at `i` (the DISPATCH subscript form — no nested brackets):
|
||
/// position after `]`, or None.
|
||
fn subscript_span(s: &[u8], i: usize) -> Option<usize> {
|
||
if s.get(i) != Some(&b'[') {
|
||
return None;
|
||
}
|
||
let mut j = i + 1;
|
||
while j < s.len() && s[j] != b']' && s[j] != b'[' {
|
||
j += 1;
|
||
}
|
||
if j < s.len() && s[j] == b']' {
|
||
Some(j + 1)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// `\s*(?:->|\.)\s*` at `i` → position after.
|
||
#[inline]
|
||
fn arrow_tail(s: &[u8], i: usize) -> Option<usize> {
|
||
let a = arrow_at(s, skip_jsws(s, i))?;
|
||
Some(skip_jsws(s, a))
|
||
}
|
||
|
||
/// ARRAY_DISPATCH_RE: /(?:\(\s*\*\s*)?\b(\w+)\s*\[[^\][]*\]\s*\)?\s*\(/g
|
||
fn scan_array_dispatch(s: &[u8], out: &mut Vec<String>) {
|
||
let mut pos = 0usize;
|
||
while pos < s.len() {
|
||
let b = s[pos];
|
||
if b != b'(' && !(is_word(b) && boundary_before(s, pos)) {
|
||
pos += 1;
|
||
continue;
|
||
}
|
||
let name_start = if b == b'(' {
|
||
let i = skip_jsws(s, pos + 1);
|
||
if s.get(i) == Some(&b'*') {
|
||
let j = skip_jsws(s, i + 1);
|
||
// \b holds: the previous char is `*` or whitespace.
|
||
if is_word_at(s, j) { Some(j) } else { None }
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
Some(pos)
|
||
};
|
||
let matched = name_start.and_then(|ns| {
|
||
let ne = word_end(s, ns);
|
||
let sub = subscript_span(s, skip_jsws(s, ne))?;
|
||
let end = close_call_tail(s, sub)?;
|
||
Some(((ns, ne), end))
|
||
});
|
||
match matched {
|
||
Some(((ns, ne), end)) => {
|
||
push_str(out, &s[ns..ne]);
|
||
pos = end;
|
||
}
|
||
None => pos += 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// INCLUDE_RE over RAW text: /#[ \t]*include[ \t]+"([^"\n]+)"/g
|
||
fn scan_includes(raw: &[u8], out: &mut Vec<String>) {
|
||
let mut pos = 0usize;
|
||
while pos < raw.len() {
|
||
let Some(h) = find_bytes(raw, b"#", pos) else { break };
|
||
let mut i = skip_sp_tab(raw, h + 1);
|
||
if raw.len() < i + 7 || &raw[i..i + 7] != b"include" {
|
||
pos = h + 1;
|
||
continue;
|
||
}
|
||
i += 7;
|
||
let q = skip_sp_tab(raw, i);
|
||
if q == i || raw.get(q) != Some(&b'"') {
|
||
pos = h + 1;
|
||
continue;
|
||
}
|
||
let mut j = q + 1;
|
||
while j < raw.len() && raw[j] != b'"' && raw[j] != b'\n' {
|
||
j += 1;
|
||
}
|
||
if j > q + 1 && j < raw.len() && raw[j] == b'"' {
|
||
out.push(bytes_to_string(&raw[q + 1..j]));
|
||
pos = j + 1;
|
||
} else {
|
||
pos = h + 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- struct field parsing ----------
|
||
|
||
/// splitTopLevel(body, sep): split on `sep` at brace/paren/bracket depth 0.
|
||
fn split_top_level(body: &[u8], sep: u8) -> Vec<Range> {
|
||
let mut out = Vec::new();
|
||
let mut depth = 0i64;
|
||
let mut start = 0usize;
|
||
for (i, &c) in body.iter().enumerate() {
|
||
match c {
|
||
b'{' | b'(' | b'[' => depth += 1,
|
||
b'}' | b')' | b']' => depth -= 1,
|
||
_ if c == sep && depth == 0 => {
|
||
out.push((start, i));
|
||
start = i + 1;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
out.push((start, body.len()));
|
||
out
|
||
}
|
||
|
||
/// JS String.prototype.trim over bytes (the JS set == our jsws set).
|
||
fn jsws_trim(s: &[u8], mut a: usize, mut b: usize) -> (usize, usize) {
|
||
loop {
|
||
let l = jsws_len(s, a);
|
||
if l == 0 || a + l > b {
|
||
break;
|
||
}
|
||
a += l;
|
||
}
|
||
// Trailing: walk from the front to find the last non-ws position (ws
|
||
// lengths vary, so scan forward tracking the end of the last non-ws char).
|
||
let mut i = a;
|
||
let mut last_end = a;
|
||
while i < b {
|
||
let l = jsws_len(s, i);
|
||
if l == 0 {
|
||
i += 1;
|
||
last_end = i;
|
||
} else {
|
||
i += l;
|
||
}
|
||
}
|
||
b = last_end;
|
||
(a, b)
|
||
}
|
||
|
||
/// /(\w+)\s+\**\s*(\w+)\s*$/ — leftmost match whose tail reaches the end.
|
||
/// Deterministic per start (greedy words/ws cannot backtrack usefully);
|
||
/// candidate starts advance one byte at a time like the JS engine.
|
||
fn first_typed(part: &[u8]) -> Option<(Range, Range)> {
|
||
let n = part.len();
|
||
let mut p = 0usize;
|
||
while p < n {
|
||
if !is_word(part[p]) {
|
||
p += 1;
|
||
continue;
|
||
}
|
||
let te = word_end(part, p);
|
||
let w = skip_jsws(part, te);
|
||
if w == te {
|
||
p += 1;
|
||
continue;
|
||
}
|
||
let mut q = w;
|
||
while q < n && part[q] == b'*' {
|
||
q += 1;
|
||
}
|
||
let q = skip_jsws(part, q);
|
||
if is_word_at(part, q) {
|
||
let ne = word_end(part, q);
|
||
let t = skip_jsws(part, ne);
|
||
if t == n {
|
||
return Some(((p, te), (q, ne)));
|
||
}
|
||
}
|
||
p += 1;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// FNPTR_DECL_RE (first match): /\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/
|
||
fn fnptr_decl(part: &[u8]) -> Option<Range> {
|
||
let mut i = 0usize;
|
||
while i < part.len() {
|
||
if part[i] == b'(' {
|
||
if let Some((name, _)) = fnptr_paren_tail(part, i) {
|
||
return Some(name);
|
||
}
|
||
}
|
||
i += 1;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Port of `parseStructFieldsRaw` — structure only, classification TS-side.
|
||
pub fn parse_struct_fields_raw(inner: &[u8]) -> Vec<RawField> {
|
||
let mut fields = Vec::new();
|
||
let mut idx: u32 = 0;
|
||
for (ds, de) in split_top_level(inner, b';') {
|
||
let (ds, de) = jsws_trim(inner, ds, de);
|
||
if ds >= de {
|
||
continue;
|
||
}
|
||
let decl = &inner[ds..de];
|
||
let parts = split_top_level(decl, b',');
|
||
let ft = first_typed(&decl[parts[0].0..parts[0].1]);
|
||
let shared_type: &[u8] = match &ft {
|
||
Some(((ts, te), _)) => &decl[parts[0].0 + ts..parts[0].0 + te],
|
||
None => b"",
|
||
};
|
||
for (pi, &(ps, pe)) in parts.iter().enumerate() {
|
||
let (ps2, pe2) = jsws_trim(decl, ps, pe);
|
||
let p = &decl[ps2..pe2];
|
||
let mut name: &[u8] = b"";
|
||
let mut ty: &[u8] = b"";
|
||
let mut ptr = false;
|
||
if let Some((ns, ne)) = fnptr_decl(p) {
|
||
name = &p[ns..ne];
|
||
ptr = true;
|
||
} else if pi == 0 {
|
||
if let Some((_, (ns, ne))) = &ft {
|
||
name = &decl[parts[0].0 + ns..parts[0].0 + ne];
|
||
ty = shared_type;
|
||
}
|
||
} else {
|
||
// /^\**\s*(\w+)/
|
||
let mut q = 0usize;
|
||
while q < p.len() && p[q] == b'*' {
|
||
q += 1;
|
||
}
|
||
let q = skip_jsws(p, q);
|
||
if is_word_at(p, q) {
|
||
name = &p[q..word_end(p, q)];
|
||
ty = shared_type;
|
||
}
|
||
}
|
||
fields.push(RawField {
|
||
name: bytes_to_string(name),
|
||
index: idx,
|
||
ptr,
|
||
ty: bytes_to_string(ty),
|
||
});
|
||
idx += 1;
|
||
}
|
||
}
|
||
fields
|
||
}
|
||
|
||
// ---------- per-file entry ----------
|
||
|
||
fn push_str(out: &mut Vec<String>, bytes: &[u8]) {
|
||
out.push(bytes_to_string(bytes));
|
||
}
|
||
|
||
#[inline]
|
||
fn bytes_to_string(bytes: &[u8]) -> String {
|
||
// All slice boundaries land on ASCII delimiters, so the content is valid
|
||
// UTF-8 whenever the input string was; lossy keeps us total anyway.
|
||
String::from_utf8_lossy(bytes).into_owned()
|
||
}
|
||
|
||
fn dedup_in_order(v: Vec<String>) -> Vec<String> {
|
||
let mut seen = std::collections::HashSet::new();
|
||
let mut out = Vec::with_capacity(v.len());
|
||
for x in v {
|
||
if seen.insert(x.clone()) {
|
||
out.push(x);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Line start offsets (byte offset of each line's first byte).
|
||
fn line_starts(s: &[u8]) -> Vec<usize> {
|
||
let mut out = vec![0usize];
|
||
for (i, &b) in s.iter().enumerate() {
|
||
if b == b'\n' {
|
||
out.push(i + 1);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Run the full extraction sweep for one file. `raw` is the file text exactly
|
||
/// as the TS side read it; `structs` are the file's struct-node extents.
|
||
pub fn scan_file(raw: &str, structs: &[StructExtent]) -> FileFacts {
|
||
let raw_b = raw.as_bytes();
|
||
let stripped = strip_c(raw_b);
|
||
let s: &[u8] = &stripped;
|
||
|
||
let mut facts = FileFacts {
|
||
fn_ptr_typedefs: Vec::new(),
|
||
fn_type_typedefs: Vec::new(),
|
||
structs: Vec::new(),
|
||
inline_ptr: false,
|
||
inline_types: Vec::new(),
|
||
inline_tags: Vec::new(),
|
||
init_tokens: Vec::new(),
|
||
array_elems: Vec::new(),
|
||
alias_names: Vec::new(),
|
||
d_pairs: Vec::new(),
|
||
dispatch_fields: Vec::new(),
|
||
array_dispatch_names: Vec::new(),
|
||
includes: Vec::new(),
|
||
};
|
||
|
||
// Typedefs (gated like the JS sweep — purely a fast path, the scans find
|
||
// nothing without the substring anyway).
|
||
if contains_bytes(s, b"typedef") {
|
||
scan_fnptr_typedefs(s, &mut facts.fn_ptr_typedefs);
|
||
scan_fntype_typedefs(s, &mut facts.fn_type_typedefs);
|
||
}
|
||
|
||
// Struct-node field declarations.
|
||
if !structs.is_empty() {
|
||
let lines = line_starts(s);
|
||
for st in structs {
|
||
let mut sf = StructFields { id: st.id.clone(), parsed: false, fields: Vec::new() };
|
||
// sliceLinesPre: falsy startLine → '' (never parses). end_line
|
||
// arrives with the TS side's `?? startLine` already applied; a
|
||
// slice whose end ≤ start is empty, exactly like Array.slice.
|
||
if st.start_line >= 1 {
|
||
let a = (st.start_line - 1) as usize;
|
||
let b = st.end_line as usize;
|
||
if a < lines.len() && b > a {
|
||
let body_start = lines[a];
|
||
// End of line (b-1): next line start minus the `\n`, or EOF.
|
||
let body_end = if b < lines.len() { lines[b] - 1 } else { s.len() };
|
||
let body = &s[body_start..body_end.max(body_start)];
|
||
if let Some(open) = body.iter().position(|&c| c == b'{') {
|
||
if let Some(close) = match_brace(body, open) {
|
||
sf.parsed = true;
|
||
sf.fields = parse_struct_fields_raw(&body[open + 1..close]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
facts.structs.push(sf);
|
||
}
|
||
}
|
||
|
||
// Registration filters.
|
||
if contains_bytes(s, b"{") {
|
||
let inline = scan_inline_structs(s);
|
||
facts.inline_ptr = inline.ptr;
|
||
facts.inline_types = dedup_in_order(inline.types);
|
||
facts.inline_tags = dedup_in_order(inline.tags);
|
||
if contains_bytes(s, b"=") {
|
||
scan_anchored(s, init_body, &mut facts.init_tokens);
|
||
facts.init_tokens = dedup_in_order(std::mem::take(&mut facts.init_tokens));
|
||
scan_anchored(s, array_table_body, &mut facts.array_elems);
|
||
facts.array_elems = dedup_in_order(std::mem::take(&mut facts.array_elems));
|
||
}
|
||
}
|
||
|
||
// Alias-shaped object macros.
|
||
if contains_bytes(s, b"#define") || contains_bytes(s, b"# define") {
|
||
scan_alias_names(s, &mut facts.alias_names);
|
||
facts.alias_names = dedup_in_order(std::mem::take(&mut facts.alias_names));
|
||
}
|
||
|
||
// Propagation + dispatch filters.
|
||
if contains_bytes(s, b"=") {
|
||
scan_field_assign(s, &mut facts.d_pairs);
|
||
facts.d_pairs = dedup_in_order(std::mem::take(&mut facts.d_pairs));
|
||
}
|
||
scan_dispatch(s, &mut facts.dispatch_fields);
|
||
facts.dispatch_fields = dedup_in_order(std::mem::take(&mut facts.dispatch_fields));
|
||
scan_array_dispatch(s, &mut facts.array_dispatch_names);
|
||
facts.array_dispatch_names = dedup_in_order(std::mem::take(&mut facts.array_dispatch_names));
|
||
|
||
// Includes come from the RAW text (string contents survive there).
|
||
if contains_bytes(raw_b, b"include") {
|
||
scan_includes(raw_b, &mut facts.includes);
|
||
}
|
||
|
||
facts
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn facts(src: &str) -> FileFacts {
|
||
scan_file(src, &[])
|
||
}
|
||
|
||
#[test]
|
||
fn strip_blanks_comments_keeps_strings() {
|
||
let s = strip_c(b"a /* x\ny */ b // c\nd \"in//str\" e");
|
||
assert_eq!(&s, b"a \n b \nd \"in//str\" e".as_slice());
|
||
}
|
||
|
||
#[test]
|
||
fn typedef_forms() {
|
||
let f = facts("typedef void (*hook_fn)(int);\ntypedef void redisCommandProc(int c);\n");
|
||
assert_eq!(f.fn_ptr_typedefs, vec!["hook_fn"]);
|
||
assert_eq!(f.fn_type_typedefs, vec!["redisCommandProc"]);
|
||
}
|
||
|
||
#[test]
|
||
fn init_modifier_backtrack() {
|
||
// `static x = {` must match with type token `static` (the JS engine
|
||
// backtracks the modifier loop) — harmless downstream, but collected.
|
||
let f = facts("; static x = {1};\n; static struct cmd t[] = { {0} };");
|
||
assert!(f.init_tokens.contains(&"static".to_string()));
|
||
assert!(f.init_tokens.contains(&"cmd".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn dispatch_backtracks_segments() {
|
||
let f = facts("int go(struct c *x){ x->cmd->proc(1); tbl[i](2); (*ops[k])(3); }");
|
||
assert!(f.dispatch_fields.contains(&"proc".to_string()));
|
||
assert!(f.array_dispatch_names.contains(&"tbl".to_string()));
|
||
assert!(f.array_dispatch_names.contains(&"ops".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn field_assign_pairs() {
|
||
let f = facts("void g(void){ a->f = b->g; h.x = k.y; m == n; }");
|
||
assert!(f.d_pairs.contains(&"f\0g".to_string()));
|
||
assert!(f.d_pairs.contains(&"x\0y".to_string()));
|
||
assert_eq!(f.d_pairs.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn alias_shapes() {
|
||
let f = facts("#define A redisCommand\n#define B struct foo\n#define C 0x12\n#define D(x) x\n");
|
||
assert!(f.alias_names.contains(&"A".to_string()));
|
||
assert!(f.alias_names.contains(&"B".to_string()));
|
||
assert!(!f.alias_names.contains(&"C".to_string()));
|
||
assert!(!f.alias_names.contains(&"D".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn includes_from_raw() {
|
||
let f = facts("#include \"commands.def\"\n// #include \"in-comment.h\"\n");
|
||
// Raw-text scan: the commented include IS captured (parity with the
|
||
// JS INCLUDE_RE over raw text).
|
||
assert_eq!(f.includes, vec!["commands.def", "in-comment.h"]);
|
||
}
|
||
}
|